find_package(Sphinx)
find_package(PdfLatex)
find_package(OpenSSL 1.1 REQUIRED )
+ find_package(PkgConfig REQUIRED)
+ pkg_check_modules(OPENSC-LIBP11 libp11 REQUIRED IMPORTED_TARGET)
- find_package(ZLIB REQUIRED)
- find_package(SQLite3 3.9.0 REQUIRED)
+ set(ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH "" CACHE PATH "Path to the driver for end-to-end encryption token")
+ option(CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN "Enforce use of an hardware token for end-to-end encryption" false)
- if(NOT WIN32 AND NOT APPLE)
- find_package(PkgConfig REQUIRED)
- pkg_check_modules(CLOUDPROVIDERS cloudproviders IMPORTED_TARGET)
+ find_package(ZLIB REQUIRED)
+ find_package(SQLite3 3.9.0 REQUIRED)
- if(CLOUDPROVIDERS_FOUND)
- pkg_check_modules(DBUS-1 REQUIRED dbus-1 IMPORTED_TARGET)
- pkg_check_modules(GIO REQUIRED gio-2.0 IMPORTED_TARGET)
- pkg_check_modules(GLIB2 REQUIRED glib-2.0 IMPORTED_TARGET)
- endif()
- endif()
+ if(NOT WIN32 AND NOT APPLE)
+ find_package(PkgConfig REQUIRED)
+ pkg_check_modules(CLOUDPROVIDERS cloudproviders IMPORTED_TARGET)
+
+ if(CLOUDPROVIDERS_FOUND)
+ pkg_check_modules(DBUS-1 REQUIRED dbus-1 IMPORTED_TARGET)
+ pkg_check_modules(GIO REQUIRED gio-2.0 IMPORTED_TARGET)
+ pkg_check_modules(GLIB2 REQUIRED glib-2.0 IMPORTED_TARGET)
+ endif()
+ endif()
endif()
option(BUILD_WITH_WEBENGINE "BUILD_WITH_WEBENGINE" ON)
set( APPLICATION_WIZARD_HEADER_TITLE_COLOR "#ffffff" CACHE STRING "Hex color of the text in the wizard header")
option( APPLICATION_WIZARD_USE_CUSTOM_LOGO "Use the logo from ':/client/theme/colored/wizard_logo.(png|svg)' else the default application icon is used" ON )
-
#
## Windows Shell Extensions & MSI - IMPORTANT: Generate new GUIDs for custom builds with "guidgen" or "uuidgen"
#
#cmakedefine WITH_WEBENGINE
+#cmakedefine01 CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN
+
+#cmakedefine ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH "@ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH@"
+
#endif
<file>src/gui/UserStatusSelectorButton.qml</file>
<file>src/gui/PredefinedStatusButton.qml</file>
<file>src/gui/ErrorBox.qml</file>
+ <file>src/gui/EncryptionTokenSelectionWindow.qml</file>
<file>src/gui/filedetails/FileActivityView.qml</file>
<file>src/gui/filedetails/FileDetailsPage.qml</file>
<file>src/gui/filedetails/FileDetailsView.qml</file>
<file>src/gui/tray/TalkReplyTextField.qml</file>
<file>src/gui/tray/CallNotificationDialog.qml</file>
<file>src/gui/tray/EditFileLocallyLoadingDialog.qml</file>
+ <file>src/gui/tray/EncryptionTokenDiscoveryDialog.qml</file>
<file>src/gui/tray/NCBusyIndicator.qml</file>
<file>src/gui/tray/NCIconWithBackgroundImage.qml</file>
<file>src/gui/tray/NCToolTip.qml</file>
#define GET_FILE_RECORD_QUERY \
"SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \
- " ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \
- " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, sharedByMe, isLivePhoto, livePhotoFile" \
+ " ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, " \
+ " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, " \
+ " sharedByMe, isLivePhoto, livePhotoFile" \
" FROM metadata" \
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
rec._checksumHeader = query.baValue(9);
rec._e2eMangledName = query.baValue(10);
rec._e2eEncryptionStatus = static_cast<SyncJournalFileRecord::EncryptionStatus>(query.intValue(11));
- rec._lockstate._locked = query.intValue(12) > 0;
- rec._lockstate._lockOwnerDisplayName = query.stringValue(13);
- rec._lockstate._lockOwnerId = query.stringValue(14);
- rec._lockstate._lockOwnerType = query.int64Value(15);
- rec._lockstate._lockEditorApp = query.stringValue(16);
- rec._lockstate._lockTime = query.int64Value(17);
- rec._lockstate._lockTimeout = query.int64Value(18);
- rec._lockstate._lockToken = query.stringValue(19);
- rec._isShared = query.intValue(20) > 0;
- rec._lastShareStateFetchedTimestamp = query.int64Value(21);
- rec._sharedByMe = query.intValue(22) > 0;
- rec._isLivePhoto = query.intValue(23) > 0;
- rec._livePhotoFile = query.stringValue(24);
+ rec._e2eCertificateFingerprint = query.baValue(12);
+ rec._lockstate._locked = query.intValue(13) > 0;
+ rec._lockstate._lockOwnerDisplayName = query.stringValue(14);
+ rec._lockstate._lockOwnerId = query.stringValue(15);
+ rec._lockstate._lockOwnerType = query.int64Value(16);
+ rec._lockstate._lockEditorApp = query.stringValue(17);
+ rec._lockstate._lockTime = query.int64Value(18);
+ rec._lockstate._lockTimeout = query.int64Value(19);
+ rec._lockstate._lockToken = query.stringValue(20);
+ rec._isShared = query.intValue(21) > 0;
+ rec._lastShareStateFetchedTimestamp = query.int64Value(22);
+ rec._sharedByMe = query.intValue(23) > 0;
+ rec._isLivePhoto = query.intValue(24) > 0;
+ rec._livePhotoFile = query.stringValue(25);
}
static QByteArray defaultJournalMode(const QString &dbPath)
addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER"));
+ addColumn(QStringLiteral("e2eCertificateFingerprint"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("sharedByMe"), QStringLiteral("INTEGER"));
const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata "
"(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, "
- "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, "
+ "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, lock, lockType, lockOwnerDisplayName, lockOwnerId, "
"lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, sharedByMe, isLivePhoto, livePhotoFile) "
- "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31);"),
+ "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32);"),
_db);
if (!query) {
qCDebug(lcDb) << "database error:" << query->error();
query->bindValue(16, contentChecksumTypeId);
query->bindValue(17, record._e2eMangledName);
query->bindValue(18, static_cast<int>(record._e2eEncryptionStatus));
- query->bindValue(19, record._lockstate._locked ? 1 : 0);
- query->bindValue(20, record._lockstate._lockOwnerType);
- query->bindValue(21, record._lockstate._lockOwnerDisplayName);
- query->bindValue(22, record._lockstate._lockOwnerId);
- query->bindValue(23, record._lockstate._lockEditorApp);
- query->bindValue(24, record._lockstate._lockTime);
- query->bindValue(25, record._lockstate._lockTimeout);
- query->bindValue(26, record._lockstate._lockToken);
- query->bindValue(27, record._isShared);
- query->bindValue(28, record._lastShareStateFetchedTimestamp);
- query->bindValue(29, record._sharedByMe);
- query->bindValue(30, record._isLivePhoto);
- query->bindValue(31, record._livePhotoFile);
+ query->bindValue(19, record._e2eCertificateFingerprint);
+ query->bindValue(20, record._lockstate._locked ? 1 : 0);
+ query->bindValue(21, record._lockstate._lockOwnerType);
+ query->bindValue(22, record._lockstate._lockOwnerDisplayName);
+ query->bindValue(23, record._lockstate._lockOwnerId);
+ query->bindValue(24, record._lockstate._lockEditorApp);
+ query->bindValue(25, record._lockstate._lockTime);
+ query->bindValue(26, record._lockstate._lockTimeout);
+ query->bindValue(27, record._lockstate._lockToken);
+ query->bindValue(28, record._isShared);
+ query->bindValue(29, record._lastShareStateFetchedTimestamp);
+ query->bindValue(30, record._sharedByMe);
+ query->bindValue(31, record._isLivePhoto);
+ query->bindValue(32, record._livePhotoFile);
if (!query->exec()) {
qCDebug(lcDb) << "database error:" << query->error();
SyncJournalDb::PinStateInterface SyncJournalDb::internalPinStates()
{
- return {this};
+ return PinStateInterface{this};
}
void SyncJournalDb::commit(const QString &context, bool startTrans)
*/
struct OCSYNC_EXPORT PinStateInterface
{
+ explicit PinStateInterface(SyncJournalDb *db)
+ : _db(db)
+ {
+ }
+
PinStateInterface(const PinStateInterface &) = delete;
PinStateInterface(PinStateInterface &&) = delete;
QByteArray _checksumHeader;
QByteArray _e2eMangledName;
EncryptionStatus _e2eEncryptionStatus = EncryptionStatus::NotEncrypted;
+ QByteArray _e2eCertificateFingerprint;
SyncJournalFileLockInfo _lockstate;
bool _isShared = false;
qint64 _lastShareStateFetchedTimestamp = 0;
* the csync state of a file.
*/
enum SyncInstructions {
- CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */
- CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */
- CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */
- CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */
- CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */
- CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */
- CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */
- CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */
- CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */
- CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
- CSYNC_INSTRUCTION_ERROR = 1 << 8,
- CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
- Used when the type of something changes from directory to file
- or back. */
- CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
- but without any propagation (UPDATE|RECONCILE) */
- CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT = 1 << 12, /* The file need to be downloaded because it is a case clash conflict (RECONCILE) */
- CSYNC_INSTRUCTION_UPDATE_VFS_METADATA = 1 << 13, /* vfs item metadata are out of sync and we need to tell operating system about it */
+ CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */
+ CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */
+ CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */
+ CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */
+ CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */
+ CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */
+ CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */
+ CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */
+ CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */
+ CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
+ CSYNC_INSTRUCTION_ERROR = 1 << 8,
+ CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
+ Used when the type of something changes from directory to file
+ or back. */
+ CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
+ but without any propagation (UPDATE|RECONCILE) */
+ CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT = 1 << 12, /* The file need to be downloaded because it is a case clash conflict (RECONCILE) */
+ CSYNC_INSTRUCTION_UPDATE_VFS_METADATA = 1 << 13, /* vfs item metadata are out of sync and we need to tell operating system about it */
+ CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA = 1 << 14, /* encryption metadata needs update after certificate was migrated */
};
Q_ENUM_NS(SyncInstructions)
syncrunfilelog.cpp
systray.h
systray.cpp
+ EncryptionTokenSelectionWindow.qml
thumbnailjob.h
thumbnailjob.cpp
userinfo.h
--- /dev/null
+/*
+ * Copyright (C) 2023 by Matthieu Gallien <matthieu.gallien@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+import QtQuick 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+import QtQml.Models 2.15
+
+import com.nextcloud.desktopclient 1.0
+import Style 1.0
+
+import "./tray"
+
+ApplicationWindow {
+ id: encryptionKeyChooserDialog
+
+ required property var certificatesInfo
+ required property ClientSideEncryptionTokenSelector certificateSelector
+ property string selectedSerialNumber: ''
+
+ flags: Qt.Window | Qt.Dialog
+ visible: true
+ modality: Qt.ApplicationModal
+
+ width: 400
+ height: 600
+ minimumWidth: 400
+ minimumHeight: 600
+
+ title: qsTr('Token Encryption Key Chooser')
+
+ // TODO: Rather than setting all these palette colours manually,
+ // create a custom style and do it for all components globally
+ palette {
+ text: Style.ncTextColor
+ windowText: Style.ncTextColor
+ buttonText: Style.ncTextColor
+ brightText: Style.ncTextBrightColor
+ highlight: Style.lightHover
+ highlightedText: Style.ncTextColor
+ light: Style.lightHover
+ midlight: Style.ncSecondaryTextColor
+ mid: Style.darkerHover
+ dark: Style.menuBorder
+ button: Style.buttonBackgroundColor
+ window: Style.backgroundColor
+ base: Style.backgroundColor
+ toolTipBase: Style.backgroundColor
+ toolTipText: Style.ncTextColor
+ }
+
+ onClosing: function(close) {
+ Systray.destroyDialog(self);
+ close.accepted = true
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 20
+ anchors.rightMargin: 20
+ anchors.bottomMargin: 20
+ anchors.topMargin: 20
+ spacing: 15
+ z: 2
+
+ EnforcedPlainTextLabel {
+ text: qsTr("Available Keys for end-to-end Encryption:")
+ font.bold: true
+ font.pixelSize: Style.bigFontPixelSizeResolveConflictsDialog
+ Layout.fillWidth: true
+ }
+
+ ScrollView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ clip: true
+
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+
+ ListView {
+ id: tokensListView
+
+ currentIndex: -1
+
+ model: DelegateModel {
+ model: certificatesInfo
+
+ delegate: ItemDelegate {
+ width: tokensListView.contentItem.width
+
+ text: modelData.subject
+
+ highlighted: tokensListView.currentIndex === index
+
+ onClicked: function()
+ {
+ tokensListView.currentIndex = index
+ selectedSerialNumber = modelData.serialNumber
+ }
+ }
+ }
+ }
+ }
+
+ DialogButtonBox {
+ Layout.fillWidth: true
+
+ Button {
+ text: qsTr("Choose")
+ DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+ }
+ Button {
+ text: qsTr("Cancel")
+ DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
+ }
+
+ onAccepted: function() {
+ Systray.destroyDialog(encryptionKeyChooserDialog)
+ certificateSelector.serialNumber = selectedSerialNumber
+ }
+
+ onRejected: function() {
+ Systray.destroyDialog(encryptionKeyChooserDialog)
+ certificateSelector.serialNumber = ''
+ }
+ }
+ }
+
+ Rectangle {
+ color: Style.backgroundColor
+ anchors.fill: parent
+ z: 1
+ }
+}
constexpr auto networkDownloadLimitSettingC = "networkDownloadLimitSetting";
constexpr auto networkUploadLimitC = "networkUploadLimit";
constexpr auto networkDownloadLimitC = "networkDownloadLimit";
+constexpr auto encryptionCertificateSha256FingerprintC = "encryptionCertificateSha256Fingerprint";
constexpr auto generalC = "General";
constexpr auto dummyAuthTypeC = "dummy";
settings.setValue(QLatin1String(serverColorC), account->_serverColor);
settings.setValue(QLatin1String(serverTextColorC), account->_serverTextColor);
settings.setValue(QLatin1String(serverHasValidSubscriptionC), account->serverHasValidSubscription());
-
+ settings.setValue(QLatin1String(encryptionCertificateSha256FingerprintC), account->encryptionCertificateFingerprint());
if (!account->_skipE2eeMetadataChecksumValidation) {
settings.remove(QLatin1String(skipE2eeMetadataChecksumValidationC));
} else {
});
job->start();
+ acc->setEncryptionCertificateFingerprint(settings.value(QLatin1String(encryptionCertificateSha256FingerprintC)).toByteArray());
+
// now the server cert, it is in the general group
settings.beginGroup(QLatin1String(generalC));
const auto certs = QSslCertificate::fromData(settings.value(caCertsKeyC).toByteArray());
constexpr auto e2EeUiActionEnableEncryptionId = "enable_encryption";
constexpr auto e2EeUiActionDisableEncryptionId = "disable_encryption";
constexpr auto e2EeUiActionDisplayMnemonicId = "display_mnemonic";
+constexpr auto e2EeUiActionMigrateCertificateId = "migrate_certificate";
}
namespace OCC {
this, &AccountSettings::slotUpdateQuota);
customizeStyle();
+
+ connect(_accountState->account()->e2e(), &ClientSideEncryption::startingDiscoveryEncryptionUsbToken,
+ Systray::instance(), &Systray::createEncryptionTokenDiscoveryDialog);
+ connect(_accountState->account()->e2e(), &ClientSideEncryption::finishedDiscoveryEncryptionUsbToken,
+ Systray::instance(), &Systray::destroyEncryptionTokenDiscoveryDialog);
}
void AccountSettings::slotE2eEncryptionMnemonicReady()
disableEncryptionForAccount(_accountState->account());
});
- const auto actionDisplayMnemonic = addActionToEncryptionMessage(tr("Display mnemonic"), e2EeUiActionDisplayMnemonicId);
- connect(actionDisplayMnemonic, &QAction::triggered, this, [this]() {
- displayMnemonic(_accountState->account()->e2e()->_mnemonic);
- });
+ if (_accountState->account()->e2e()->userCertificateNeedsMigration()) {
+ slotE2eEncryptionCertificateNeedMigration();
+ }
+
+ if (!_accountState->account()->e2e()->getMnemonic().isEmpty()) {
+ const auto actionDisplayMnemonic = addActionToEncryptionMessage(tr("Display mnemonic"), e2EeUiActionDisplayMnemonicId);
+ connect(actionDisplayMnemonic, &QAction::triggered, this, [this]() {
+ displayMnemonic(_accountState->account()->e2e()->getMnemonic());
+ });
+ }
_ui->encryptionMessage->setMessageType(KMessageWidget::Positive);
_ui->encryptionMessage->setText(tr("End-to-end encryption has been enabled for this account"));
connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotE2eEncryptionInitializationFinished);
_accountState->account()->setE2eEncryptionKeysGenerationAllowed(true);
_accountState->account()->setAskUserForMnemonic(true);
- _accountState->account()->e2e()->initialize(_accountState->account());
+ _accountState->account()->e2e()->initialize(this, _accountState->account());
}
void AccountSettings::slotE2eEncryptionInitializationFinished(bool isNewMnemonicGenerated)
{
disconnect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotE2eEncryptionInitializationFinished);
- if (!_accountState->account()->e2e()->_mnemonic.isEmpty()) {
+ if (_accountState->account()->e2e()->isInitialized()) {
removeActionFromEncryptionMessage(e2EeUiActionEnableEncryptionId);
slotE2eEncryptionMnemonicReady();
if (isNewMnemonicGenerated) {
- displayMnemonic(_accountState->account()->e2e()->_mnemonic);
+ displayMnemonic(_accountState->account()->e2e()->getMnemonic());
}
+ Q_EMIT _accountState->account()->wantsFoldersSynced();
}
_accountState->account()->setAskUserForMnemonic(false);
}
return false;
}
- if (!_accountState->account()->e2e() || _accountState->account()->e2e()->_mnemonic.isEmpty()) {
+ if (!_accountState->account()->e2e() || !_accountState->account()->e2e()->isInitialized()) {
QMessageBox msgBox;
msgBox.setText(tr("End-to-end encryption is not configured on this device. "
"Once it is configured, you will be able to encrypt this folder.\n"
}
}
+void AccountSettings::migrateCertificateForAccount(const AccountPtr &account)
+{
+ for (const auto action : _ui->encryptionMessage->actions()) {
+ _ui->encryptionMessage->removeAction(action);
+ }
+
+ account->e2e()->migrateCertificate();
+ slotE2eEncryptionGenerateKeys();
+}
+
void AccountSettings::showConnectionLabel(const QString &message, QStringList errors)
{
const auto errStyle = QLatin1String("color:#ffffff; background-color:#bb4d4d;padding:5px;"
void AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync()
{
- if (_accountState->account()->e2e()->_mnemonic.isEmpty()) {
+ if (!_accountState->account()->e2e()->isInitialized()) {
return;
}
}
}
+void AccountSettings::slotE2eEncryptionCertificateNeedMigration()
+{
+ const auto actionMigrateCertificate = addActionToEncryptionMessage(tr("Migrate certificate to a new one"), e2EeUiActionMigrateCertificateId);
+ connect(actionMigrateCertificate, &QAction::triggered, this, [this] {
+ migrateCertificateForAccount(_accountState->account());
+ });
+}
+
void AccountSettings::updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const
{
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList);
{
connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync);
- if (!_accountState->account()->e2e()->_mnemonic.isEmpty()) {
+ if (_accountState->account()->e2e()->isInitialized()) {
slotE2eEncryptionMnemonicReady();
} else {
initializeE2eEncryptionSettingsMessage();
connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, [this] {
- if (!_accountState->account()->e2e()->_publicKey.isNull()) {
+ if (!_accountState->account()->e2e()->getPublicKey().isNull()) {
_ui->encryptionMessage->setText(tr("End-to-end encryption has been enabled on this account with another device."
"<br>"
"It can be enabled on this device by entering your mnemonic."
}
});
_accountState->account()->setE2eEncryptionKeysGenerationAllowed(false);
- _accountState->account()->e2e()->initialize(_accountState->account());
+ _accountState->account()->e2e()->initialize(this, _accountState->account());
}
}
checkClientSideEncryptionState();
const auto account = _accountState->account();
- if (account->e2e()->_mnemonic.isEmpty()) {
+ if (!account->e2e()->isInitialized()) {
FolderMan::instance()->removeE2eFiles(account);
}
}
~AccountSettings() override;
[[nodiscard]] QSize sizeHint() const override { return ownCloudGui::settingsDialogSize(); }
bool canEncryptOrDecrypt(const FolderStatusModel::SubFolderInfo* folderInfo);
+ [[nodiscard]] OCC::AccountState *accountsState() const { return _accountState; }
signals:
void folderChanged();
void slotUpdateQuota(qint64 total, qint64 used);
void slotAccountStateChanged();
void slotStyleChanged();
- OCC::AccountState *accountsState() { return _accountState; }
void slotHideSelectiveSyncWidget();
protected slots:
const QVector<int> &roles);
void slotPossiblyUnblacklistE2EeFoldersAndRestartSync();
+ void slotE2eEncryptionCertificateNeedMigration();
+
private slots:
void updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const;
void folderTerminateSyncAndUpdateBlackList(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist);
private slots:
void displayMnemonic(const QString &mnemonic);
void disableEncryptionForAccount(const OCC::AccountPtr &account) const;
+ void migrateCertificateForAccount(const OCC::AccountPtr &account);
void showConnectionLabel(const QString &message, QStringList errors = QStringList());
void openIgnoredFilesDialog(const QString & absFolderPath);
void customizeStyle();
#ifndef TOKEN_AUTH_ONLY
connect(_account->e2e(), &ClientSideEncryption::initializationFinished, this, &ConnectionValidator::reportConnected);
- _account->e2e()->initialize(_account);
+ _account->e2e()->initialize(nullptr, _account);
#else
reportResult(Connected);
#endif
connect(_manager.data(), &ShareManager::linkShareCreated, this, &ShareModel::slotAddShare);
connect(_manager.data(), &ShareManager::linkShareRequiresPassword, this, &ShareModel::requestPasswordForLinkShare);
connect(_manager.data(), &ShareManager::serverError, this, [this](const int code, const QString &message) {
- _hasInitialShareFetchCompleted = true;
- Q_EMIT hasInitialShareFetchCompletedChanged();
+ if (!_hasInitialShareFetchCompleted) {
+ _hasInitialShareFetchCompleted = true;
+ Q_EMIT hasInitialShareFetchCompletedChanged();
+ }
+
+ qCWarning(lcShareModel) << "Error from server from ShareManager class and initShareManager" << code << message;
emit serverError(code, message);
});
const QPersistentModelIndex sharePersistentIndex(shareModelIndex);
_shareIdIndexHash.insert(shareId, sharePersistentIndex);
- connect(share.data(), &Share::serverError, this, &ShareModel::slotServerError);
+ connect(share.data(), &Share::serverError, this, [this] (int code, const QString &message) {
+ qCWarning(lcShareModel) << "Error from server from Share class" << code << message;
+ Q_EMIT serverError(code, message);
+ });
connect(share.data(), &Share::passwordSetError, this, [this, shareId](const int code, const QString &message) {
_shareIdRecentlySetPasswords.remove(shareId);
slotSharePasswordSet(shareId);
connect(userGroupShare.data(), &UserGroupShare::expireDateSet, this, [this, shareId]{ slotShareExpireDateSet(shareId); });
}
- if (_manager) {
- connect(_manager.data(), &ShareManager::serverError, this, &ShareModel::slotServerError);
- }
-
handleLinkShare();
Q_EMIT sharesChanged();
}
Q_EMIT sharesChanged();
}
-void ShareModel::slotServerError(const int code, const QString &message)
-{
- qCWarning(lcShareModel) << "Error from server" << code << message;
- Q_EMIT serverError(code, message);
-}
-
void ShareModel::slotAddSharee(const ShareePtr &sharee)
{
if(!sharee) {
void setHideDownloadEnabledChangeInProgress(const QString &shareId, const bool isInProgress);
void slotPropfindReceived(const QVariantMap &result);
- void slotServerError(const int code, const QString &message);
void slotAddShare(const OCC::SharePtr &share);
void slotRemoveShareWithId(const QString &shareId);
void slotSharesFetched(const QList<OCC::SharePtr> &shares);
connect(_accountState->account().data(), &Account::capabilitiesChanged, this, &Folder::slotCapabilitiesChanged);
+ connect(_accountState->account().data(), &Account::wantsFoldersSynced, this, [this] () {
+ _engine->setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::FilesystemOnly);
+ QMetaObject::invokeMethod(_engine.data(), "startSync", Qt::QueuedConnection);
+ });
+
// Potentially upgrade suffix vfs to windows vfs
ENFORCE(_vfs);
if (_definition.virtualFilesMode == Vfs::WithSuffix
void FolderMan::removeE2eFiles(const AccountPtr &account) const
{
- Q_ASSERT(account->e2e()->_mnemonic.isEmpty());
+ Q_ASSERT(!account->e2e()->isInitialized());
for (const auto folder : map()) {
if(folder->accountState()->account()->id() == account->id()) {
folder->removeLocalE2eFiles();
newInfo._isNonDecryptable = newInfo.isEncrypted()
&& _accountState->account()->e2e()
- && !_accountState->account()->e2e()->_publicKey.isNull()
- && _accountState->account()->e2e()->_privateKey.isNull();
+ && !_accountState->account()->e2e()->isInitialized();
SyncJournalFileRecord rec;
if (!parentInfo->_folder->journalDb()->getFileRecordByE2eMangledName(removeTrailingSlash(relativePath), &rec)) {
qmlRegisterUncreatableType<UnifiedSearchResultsListModel>("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel");
qmlRegisterUncreatableType<UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
qmlRegisterUncreatableType<Sharee>("com.nextcloud.desktopclient", 1, 0, "Sharee", "Access to Type enum");
+ qmlRegisterUncreatableType<ClientSideEncryptionTokenSelector>("com.nextcloud.desktopclient", 1, 0, "ClientSideEncryptionTokenSelector", "Access to the certificate selector");
qRegisterMetaType<ActivityListModel *>("ActivityListModel*");
qRegisterMetaType<UnifiedSearchResultsListModel *>("UnifiedSearchResultsListModel*");
if (_settingsDialog.isNull()) {
_settingsDialog = new SettingsDialog(this);
_settingsDialog->setAttribute(Qt::WA_DeleteOnClose, true);
+
#ifdef Q_OS_MAC
auto *fgbg = new ForegroundBackground();
_settingsDialog->installEventFilter(fgbg);
#endif
+
+ connect(_tray.data(), &Systray::hideSettingsDialog,
+ _settingsDialog.data(), &SettingsDialog::close);
+
_settingsDialog->show();
}
raiseDialog(_settingsDialog.data());
const auto rec = fileData.journalRecord();
Q_ASSERT(rec.isValid());
- if (!account->e2e() || account->e2e()->_mnemonic.isEmpty()) {
+ if (!account->e2e() || !account->e2e()->isInitialized()) {
const int ret = QMessageBox::critical(
nullptr,
tr("Failed to encrypt folder at \"%1\"").arg(fileData.folderRelativePath),
tr("The account %1 does not have end-to-end encryption configured. "
"Please configure this in your account settings to enable folder encryption.").arg(account->prettyName()),
QMessageBox::Ok
- );
+ );
Q_UNUSED(ret)
return;
}
#include "configfile.h"
#include "accessmanager.h"
#include "callstatechecker.h"
+#include "clientsideencryptiontokenselector.h"
#include <QCursor>
#include <QGuiApplication>
#include <QScreen>
#include <QMenu>
#include <QGuiApplication>
+#include <QQuickView>
+#include <QMessageBox>
#ifdef USE_FDO_NOTIFICATIONS
#include <QDBusConnection>
dialogWindow->requestActivate();
}
+void Systray::createEncryptionTokenDiscoveryDialog()
+{
+ if (_encryptionTokenDiscoveryDialog) {
+ return;
+ }
+
+ qCDebug(lcSystray) << "Opening an encryption token discovery dialog...";
+
+ const auto encryptionTokenDiscoveryDialog = new QQmlComponent(_trayEngine.get(), QStringLiteral("qrc:/qml/src/gui/tray/EncryptionTokenDiscoveryDialog.qml"));
+
+ if (encryptionTokenDiscoveryDialog->isError()) {
+ qCWarning(lcSystray) << encryptionTokenDiscoveryDialog->errorString();
+ return;
+ }
+
+ _encryptionTokenDiscoveryDialog = encryptionTokenDiscoveryDialog->createWithInitialProperties(QVariantMap{});
+}
+
+void Systray::destroyEncryptionTokenDiscoveryDialog()
+{
+ if (!_encryptionTokenDiscoveryDialog) {
+ return;
+ }
+ qCDebug(lcSystray) << "Closing an encryption token discovery dialog...";
+ _encryptionTokenDiscoveryDialog->deleteLater();
+ _encryptionTokenDiscoveryDialog = nullptr;
+}
+
bool Systray::raiseDialogs()
{
return raiseFileDetailDialogs();
namespace OCC {
+class ClientSideEncryptionTokenSelector;
+
class AccessManagerFactory : public QQmlNetworkAccessManagerFactory
{
public:
void syncIsPausedChanged();
void isOpenChanged();
+ void hideSettingsDialog();
+
public slots:
void setTrayEngine(QQmlApplicationEngine *trayEngine);
void create();
void createEditFileLocallyLoadingDialog(const QString &fileName);
void destroyEditFileLocallyLoadingDialog();
void createResolveConflictsDialog(const OCC::ActivityList &allConflicts);
+ void createEncryptionTokenDiscoveryDialog();
+ void destroyEncryptionTokenDiscoveryDialog();
void slotCurrentUserChanged();
QSet<qlonglong> _callsAlreadyNotified;
QPointer<QObject> _editFileLocallyLoadingDialog;
+ QPointer<QObject> _encryptionTokenDiscoveryDialog;
QVector<QQuickWindow*> _fileDetailDialogs;
+ QQuickWindow* _tokenInitDialog = nullptr;
QStringListModel _fakeActivityModel;
};
--- /dev/null
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import Style 1.0
+import com.nextcloud.desktopclient 1.0
+import QtQuick.Layouts 1.15
+import QtQuick.Controls 2.15
+
+ApplicationWindow {
+ id: root
+ flags: Qt.Dialog | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
+
+ color: "transparent"
+
+ width: 320
+ height: contentLayout.implicitHeight
+ modality: Qt.ApplicationModal
+
+ readonly property real fontPixelSize: Style.topLinePixelSize * 1.5
+ readonly property real iconWidth: fontPixelSize * 2
+
+ // TODO: Rather than setting all these palette colours manually,
+ // create a custom style and do it for all components globally
+ palette {
+ text: Style.ncTextColor
+ windowText: Style.ncTextColor
+ buttonText: Style.ncTextColor
+ brightText: Style.ncTextBrightColor
+ highlight: Style.lightHover
+ highlightedText: Style.ncTextColor
+ light: Style.lightHover
+ midlight: Style.ncSecondaryTextColor
+ mid: Style.darkerHover
+ dark: Style.menuBorder
+ button: Style.buttonBackgroundColor
+ window: Style.backgroundColor
+ base: Style.backgroundColor
+ toolTipBase: Style.backgroundColor
+ toolTipText: Style.ncTextColor
+ }
+
+ Component.onCompleted: {
+ Systray.forceWindowInit(root);
+ x = Screen.width / 2 - width / 2
+ y = Screen.height / 2 - height / 2
+ root.show();
+ root.raise();
+ root.requestActivate();
+ }
+
+ Rectangle {
+ id: windowBackground
+ color: Style.backgroundColor
+ radius: Style.trayWindowRadius
+ border.color: palette.dark
+ anchors.fill: parent
+ }
+
+ ColumnLayout {
+ id: contentLayout
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: Style.standardSpacing
+ anchors.rightMargin: Style.standardSpacing
+ spacing: Style.standardSpacing
+
+ NCBusyIndicator {
+ id: busyIndicator
+ Layout.topMargin: Style.standardSpacing
+ Layout.alignment: Qt.AlignHCenter
+ Layout.preferredWidth: root.iconWidth
+ Layout.preferredHeight: root.iconWidth
+ imageSourceSizeHeight: root.iconWidth
+ imageSourceSizeWidth: root.iconWidth
+ padding: 0
+ color: palette.windowText
+ running: true
+ }
+ EnforcedPlainTextLabel {
+ id: labelMessage
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillWidth: true
+ Layout.bottomMargin: Style.standardSpacing
+ text: qsTr("Discovering the certificates stored on your USB token")
+ elide: Text.ElideRight
+ font.pixelSize: root.fontPixelSize
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+}
// Note that these are in the order we want to present them in the model!
enum Type {
DummyFetchingActivityType,
+ OpenSettingsNotificationType,
NotificationType,
SyncResultType,
SyncFileItemType,
case Activity::DummyMoreActivitiesAvailableType:
return "Activity";
case Activity::NotificationType:
+ case Activity::OpenSettingsNotificationType:
return "Notification";
case Activity::SyncFileItemType:
return "File";
case Activity::SyncResultType:
return "Sync";
- default:
- return QVariant();
}
+ break;
}
case ActionTextRole:
if(a._subjectDisplay.isEmpty()) {
case IsCurrentUserFileActivityRole:
return a._isCurrentUserFileActivity;
case ThumbnailRole: {
- if (a._type == Activity::NotificationType && !a._talkNotificationData.userAvatar.isEmpty()) {
+ if ((a._type == Activity::NotificationType || a._type == Activity::OpenSettingsNotificationType) &&
+ !a._talkNotificationData.userAvatar.isEmpty()) {
return generateAvatarThumbnailMap(a._talkNotificationData.userAvatar);
}
return QVariant::fromValue(a);
}
- return QVariant();
+ return {};
}
int ActivityListModel::rowCount(const QModelIndex &parent) const
}
if (activity._type != Activity::ActivityType &&
- activity._type != Activity::DummyFetchingActivityType &&
- activity._type != Activity::DummyMoreActivitiesAvailableType &&
- activity._type != Activity::NotificationType) {
+ activity._type != Activity::DummyFetchingActivityType &&
+ activity._type != Activity::DummyMoreActivitiesAvailableType &&
+ activity._type != Activity::NotificationType &&
+ activity._type != Activity::OpenSettingsNotificationType) {
const auto notificationErrorsListIndex = _notificationErrorsLists.indexOf(activity);
if (notificationErrorsListIndex != -1)
_currentInvalidFilenameDialog->open();
ownCloudGui::raiseDialog(_currentInvalidFilenameDialog);
return;
+ } else if (activity._type == Activity::OpenSettingsNotificationType) {
+ Q_EMIT showSettingsDialog();
}
if (!path.isEmpty()) {
void interactiveActivityReceived();
+ void showSettingsDialog();
+
protected:
[[nodiscard]] bool currentlyFetching() const;
connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::slotAccountCapabilitiesChangedRefreshGroupFolders);
connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
-
+ connect(_activityModel, &ActivityListModel::showSettingsDialog,
+ Systray::instance(), &Systray::openSettings);
+
connect(this, &User::sendReplyMessage, this, &User::slotSendReplyMessage);
+
+ connect(_account->account().data(), &Account::userCertificateNeedsMigrationChanged, this, [this] () {
+ auto certificateNeedMigration = Activity{};
+ certificateNeedMigration._type = Activity::OpenSettingsNotificationType;
+ certificateNeedMigration._subject = tr("End-to-end certificate needs to be migrated to a new one");
+ certificateNeedMigration._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate);
+ certificateNeedMigration._message = tr("Trigger the migration");
+ certificateNeedMigration._accName = _account->account()->displayName();
+ certificateNeedMigration._id = qHash("migrate-certificate");
+
+ _activityModel->removeActivityFromActivityList(certificateNeedMigration);
+
+ if (_account->account()->e2e()->userCertificateNeedsMigration()) {
+ _activityModel->addNotificationToActivityList(certificateNeedMigration);
+ showDesktopNotification(certificateNeedMigration);
+ }
+ });
}
void User::checkNotifiedNotifications()
)
endif()
+if (WIN32)
+ list(APPEND OS_SPECIFIC_LINK_LIBRARIES
+ Crypt32
+ )
+endif()
+
set(libsync_SRCS
account.h
account.cpp
clientsideencryptionjobs.cpp
clientsideencryptionprimitives.h
clientsideencryptionprimitives.cpp
+ clientsideencryptiontokenselector.h
+ clientsideencryptiontokenselector.cpp
datetimeprovider.h
datetimeprovider.cpp
rootencryptedfolderinfo.h
Nextcloud::csync
OpenSSL::Crypto
OpenSSL::SSL
+ PkgConfig::OPENSC-LIBP11
${OS_SPECIFIC_LINK_LIBRARIES}
Qt::Core
Qt::Network
#include "clientsideencryption.h"
#include "ocsuserstatusconnector.h"
+#include "config.h"
+
#include <QLoggingCategory>
#include <QNetworkReply>
#include <QNetworkAccessManager>
_pushNotificationsReconnectTimer.setInterval(pushNotificationsReconnectInterval);
connect(&_pushNotificationsReconnectTimer, &QTimer::timeout, this, &Account::trySetupPushNotifications);
+
+ connect(&_e2e, &ClientSideEncryption::userCertificateNeedsMigrationChanged,
+ this, &Account::userCertificateNeedsMigrationChanged);
}
AccountPtr Account::create()
return _e2eAskUserForMnemonic;
}
+bool Account::enforceUseHardwareTokenEncryption() const
+{
+#if defined CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN
+ return CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN;
+#else
+ return false;
+#endif
+}
+
+QString Account::encryptionHardwareTokenDriverPath() const
+{
+#if defined ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH
+ return ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH;
+#else
+ return {};
+#endif
+}
+
+QByteArray Account::encryptionCertificateFingerprint() const
+{
+ return _encryptionCertificateFingerprint;
+}
+
+void Account::setEncryptionCertificateFingerprint(const QByteArray &fingerprint)
+{
+ if (_encryptionCertificateFingerprint == fingerprint) {
+ return;
+ }
+
+ _encryptionCertificateFingerprint = fingerprint;
+ _e2e.usbTokenInformation()->setSha256Fingerprint(fingerprint);
+ Q_EMIT encryptionCertificateFingerprintChanged();
+ Q_EMIT wantsAccountSaved(this);
+}
+
void Account::setAskUserForMnemonic(const bool ask)
{
_e2eAskUserForMnemonic = ask;
Q_PROPERTY(AccountNetworkTransferLimitSetting downloadLimitSetting READ downloadLimitSetting WRITE setDownloadLimitSetting NOTIFY downloadLimitSettingChanged)
Q_PROPERTY(unsigned int uploadLimit READ uploadLimit WRITE setUploadLimit NOTIFY uploadLimitChanged)
Q_PROPERTY(unsigned int downloadLimit READ downloadLimit WRITE setDownloadLimit NOTIFY downloadLimitChanged)
+ Q_PROPERTY(bool enforceUseHardwareTokenEncryption READ enforceUseHardwareTokenEncryption NOTIFY enforceUseHardwareTokenEncryptionChanged)
+ Q_PROPERTY(QString encryptionHardwareTokenDriverPath READ encryptionHardwareTokenDriverPath NOTIFY encryptionHardwareTokenDriverPathChanged)
+ Q_PROPERTY(QByteArray encryptionCertificateFingerprint READ encryptionCertificateFingerprint WRITE setEncryptionCertificateFingerprint NOTIFY encryptionCertificateFingerprintChanged)
public:
// We need to decide whether to use the client's global proxy settings or whether to use
[[nodiscard]] bool serverHasValidSubscription() const;
void setServerHasValidSubscription(bool valid);
+ [[nodiscard]] bool enforceUseHardwareTokenEncryption() const;
+
+ [[nodiscard]] QString encryptionHardwareTokenDriverPath() const;
+
+ [[nodiscard]] QByteArray encryptionCertificateFingerprint() const;
+ void setEncryptionCertificateFingerprint(const QByteArray &fingerprint);
+
public slots:
/// Used when forgetting credentials
void clearQNAMCache();
// e.g. when the approved SSL certificates changed
void wantsAccountSaved(OCC::Account *acc);
+ void wantsFoldersSynced();
+
void serverVersionChanged(OCC::Account *account, const QString &newVersion, const QString &oldVersion);
void accountChangedAvatar();
void accountChangedDisplayName();
void prettyNameChanged();
void askUserForMnemonicChanged();
+ void enforceUseHardwareTokenEncryptionChanged();
+ void encryptionHardwareTokenDriverPathChanged();
/// Used in RemoteWipe
void appPasswordRetrieved(QString);
void downloadLimitChanged();
void termsOfServiceNeedToBeChecked();
+ void encryptionCertificateFingerprintChanged();
+ void userCertificateNeedsMigrationChanged();
+
protected Q_SLOTS:
void slotCredentialsFetched();
void slotCredentialsAsked();
AccountNetworkTransferLimitSetting _downloadLimitSetting = AccountNetworkTransferLimitSetting::GlobalLimit;
unsigned int _uploadLimit = 0;
unsigned int _downloadLimit = 0;
-
bool _serverHasValidSubscription = false;
+ QByteArray _encryptionCertificateFingerprint;
/* IMPORTANT - remove later - FIXME MS@2019-12-07 -->
* TODO: For "Log out" & "Remove account": Remove client CA certs and KEY!
-#include "clientsideencryption.h"
+/*
+ * Copyright © 2017, Tomaz Canabrava <tcanabrava@kde.org>
+ * Copyright © 2020, Andreas Jellinghaus <andreas@ionisiert.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
-#include <openssl/rsa.h>
-#include <openssl/evp.h>
-#include <openssl/cms.h>
-#include <openssl/pem.h>
-#include <openssl/err.h>
-#include <openssl/engine.h>
-#include <openssl/rand.h>
+#include "clientsideencryption.h"
#include "account.h"
#include "capabilities.h"
#include <QXmlStreamNamespaceDeclaration>
#include <QStack>
#include <QInputDialog>
+#include <QMessageBox>
+#include <QWidget>
#include <QLineEdit>
#include <QIODevice>
#include <QUuid>
#include <QRandomGenerator>
#include <QJsonArray>
#include <QCryptographicHash>
+#include <QFutureWatcher>
+#include <QSslCertificate>
+#include <QSslCertificateExtension>
+
+#include <openssl/rsa.h>
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+#include <openssl/err.h>
+#include <openssl/engine.h>
+#include <openssl/rand.h>
+#include <openssl/cms.h>
#include <map>
#include <string>
#include <algorithm>
-
+#include <optional>
#include <cstdio>
QDebug operator<<(QDebug out, const std::string& str)
{
Q_LOGGING_CATEGORY(lcCse, "nextcloud.sync.clientsideencryption", QtInfoMsg)
-Q_LOGGING_CATEGORY(lcCseDecryption, "nextcloud.e2e", QtInfoMsg)
+Q_LOGGING_CATEGORY(lcCseDecryption, "nextcloud.sync.clientsideencryption.decryption", QtInfoMsg)
+Q_LOGGING_CATEGORY(lcCseEncryption, "nextcloud.sync.clientsideencryption.encryption", QtInfoMsg)
QString e2eeBaseUrl(const OCC::AccountPtr &account)
{
/* Create and initialise the context */
if(!ctx) {
- qCInfo(lcCse()) << "Error creating cipher";
- handleErrors();
+ qCInfo(lcCse()) << "Error creating cipher" << handleErrors();
}
/* Initialise the decryption operation. */
if(!EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr)) {
- qCInfo(lcCse()) << "Error initializing context with aes_256";
- handleErrors();
+ qCInfo(lcCse()) << "Error initializing context with aes_256" << handleErrors();
}
// No padding
/* Set IV length. */
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) {
- qCInfo(lcCse()) << "Error setting iv length";
- handleErrors();
+ qCInfo(lcCse()) << "Error setting iv length" << handleErrors();
}
/* Initialise key and IV */
if(!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (unsigned char *)key.constData(), (unsigned char *)iv.constData())) {
- qCInfo(lcCse()) << "Error initialising key and iv";
- handleErrors();
+ qCInfo(lcCse()) << "Error initialising key and iv" << handleErrors();
}
// We write the base64 encoded private key
// Do the actual encryption
int len = 0;
if(!EVP_EncryptUpdate(ctx, unsignedData(ctext), &len, (unsigned char *)privateKeyB64.constData(), privateKeyB64.size())) {
- qCInfo(lcCse()) << "Error encrypting";
- handleErrors();
+ qCInfo(lcCse()) << "Error encrypting" << handleErrors();
}
int clen = len;
* this stage, but this does not occur in GCM mode
*/
if(1 != EVP_EncryptFinal_ex(ctx, unsignedData(ctext) + len, &len)) {
- qCInfo(lcCse()) << "Error finalizing encryption";
- handleErrors();
+ qCInfo(lcCse()) << "Error finalizing encryption" << handleErrors();
}
clen += len;
/* Get the e2EeTag */
QByteArray e2EeTag(OCC::Constants::e2EeTagSize, '\0');
if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, OCC::Constants::e2EeTagSize, unsignedData(e2EeTag))) {
- qCInfo(lcCse()) << "Error getting the e2EeTag";
- handleErrors();
+ qCInfo(lcCse()) << "Error getting the e2EeTag" << handleErrors();
}
QByteArray cipherTXT;
return pem;
}
-QByteArray encryptStringAsymmetric(const QSslKey key, const QByteArray &data)
+namespace internals {
+
+[[nodiscard]] std::optional<QByteArray> encryptStringAsymmetric(ENGINE *sslEngine,
+ EVP_PKEY *publicKey,
+ int pad_mode,
+ const QByteArray& binaryData);
+
+[[nodiscard]] std::optional<QByteArray> decryptStringAsymmetric(ENGINE *sslEngine,
+ EVP_PKEY *privateKey,
+ int pad_mode,
+ const QByteArray& binaryData);
+
+}
+
+std::optional<QByteArray> encryptStringAsymmetric(const CertificateInformation &selectedCertificate,
+ const int paddingMode,
+ const ClientSideEncryption &encryptionEngine,
+ const QByteArray &binaryData)
{
- Q_ASSERT(!key.isNull());
- if (key.isNull()) {
- qCDebug(lcCse) << "Public key is null. Could not encrypt.";
+ if (!encryptionEngine.isInitialized()) {
+ qCWarning(lcCseDecryption()) << "end-to-end encryption is disabled";
return {};
}
- Bio publicKeyBio;
- const auto publicKeyPem = key.toPem();
- BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size());
- const auto publicKey = PKey::readPublicKey(publicKeyBio);
- return EncryptionHelper::encryptStringAsymmetric(publicKey, data);
+
+ if (encryptionEngine.useTokenBasedEncryption()) {
+ qCDebug(lcCseEncryption()) << "use certificate on hardware token";
+ } else {
+ qCDebug(lcCseEncryption()) << "use certificate on software storage";
+ }
+
+ const auto publicKey = selectedCertificate.getEvpPublicKey();
+ Q_ASSERT(publicKey);
+
+ auto encryptedBase64Result = internals::encryptStringAsymmetric(encryptionEngine.sslEngine(), publicKey, paddingMode, binaryData);
+
+ if (!encryptedBase64Result) {
+ qCWarning(lcCseEncryption()) << "encrypt failed";
+ return {};
+ }
+
+ if (encryptedBase64Result->isEmpty()) {
+ qCDebug(lcCseEncryption()) << "ERROR. Could not encrypt data";
+ return {};
+ }
+
+ return encryptedBase64Result;
}
-QByteArray decryptStringAsymmetric(const QByteArray &privateKeyPem, const QByteArray &data)
+std::optional<QByteArray> decryptStringAsymmetric(const CertificateInformation &selectedCertificate,
+ const int paddingMode,
+ const ClientSideEncryption &encryptionEngine,
+ const QByteArray &base64Data)
{
- Q_ASSERT(!privateKeyPem.isEmpty());
- if (privateKeyPem.isEmpty()) {
- qCDebug(lcCse) << "Private key is empty. Could not encrypt.";
+ if (!encryptionEngine.isInitialized()) {
+ qCWarning(lcCseDecryption()) << "end-to-end encryption is disabled";
return {};
}
- Bio privateKeyBio;
- BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size());
- const auto key = PKey::readPrivateKey(privateKeyBio);
+ if (encryptionEngine.useTokenBasedEncryption()) {
+ qCDebug(lcCseDecryption()) << "use certificate on hardware token";
+ } else {
+ qCDebug(lcCseDecryption()) << "use certificate on software storage";
+ }
+ const auto key = selectedCertificate.getEvpPrivateKey();
+ if (!key) {
+ qCWarning(lcCseDecryption()) << "invalid private key handle";
+ return {};
+ }
- // Also base64 decode the result
- const auto decryptResult = EncryptionHelper::decryptStringAsymmetric(key, data);
+ const auto decryptBase64Result = internals::decryptStringAsymmetric(encryptionEngine.sslEngine(), key, paddingMode, QByteArray::fromBase64(base64Data));
+ if (!decryptBase64Result) {
+ qCWarning(lcCseDecryption()) << "decrypt failed";
+ return {};
+ }
- if (decryptResult.isEmpty()) {
- qCDebug(lcCse()) << "ERROR. Could not decrypt data";
+ if (decryptBase64Result->isEmpty()) {
+ qCDebug(lcCseDecryption()) << "ERROR. Could not decrypt data";
return {};
}
- return decryptResult;
+ return decryptBase64Result;
}
QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) {
/* Create and initialise the context */
if(!ctx) {
- qCInfo(lcCse()) << "Error creating cipher";
- handleErrors();
+ qCInfo(lcCse()) << "Error creating cipher" << handleErrors();
return {};
}
/* Initialise the decryption operation. */
if(!EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) {
- qCInfo(lcCse()) << "Error initializing context with aes_128";
- handleErrors();
+ qCInfo(lcCse()) << "Error initializing context with aes_128" << handleErrors();
return {};
}
/* Set IV length. */
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) {
- qCInfo(lcCse()) << "Error setting iv length";
- handleErrors();
+ qCInfo(lcCse()) << "Error setting iv length" << handleErrors();
return {};
}
/* Initialise key and IV */
if(!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (unsigned char *)key.constData(), (unsigned char *)iv.constData())) {
- qCInfo(lcCse()) << "Error initialising key and iv";
- handleErrors();
+ qCInfo(lcCse()) << "Error initialising key and iv" << handleErrors();
return {};
}
// Do the actual encryption
int len = 0;
if(!EVP_EncryptUpdate(ctx, unsignedData(ctext), &len, (unsigned char *)dataB64.constData(), dataB64.size())) {
- qCInfo(lcCse()) << "Error encrypting";
- handleErrors();
+ qCInfo(lcCse()) << "Error encrypting" << handleErrors();
return {};
}
* this stage, but this does not occur in GCM mode
*/
if(1 != EVP_EncryptFinal_ex(ctx, unsignedData(ctext) + len, &len)) {
- qCInfo(lcCse()) << "Error finalizing encryption";
- handleErrors();
+ qCInfo(lcCse()) << "Error finalizing encryption" << handleErrors();
return {};
}
clen += len;
/* Get the e2EeTag */
QByteArray e2EeTag(OCC::Constants::e2EeTagSize, '\0');
if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, OCC::Constants::e2EeTagSize, unsignedData(e2EeTag))) {
- qCInfo(lcCse()) << "Error getting the e2EeTag";
- handleErrors();
+ qCInfo(lcCse()) << "Error getting the e2EeTag" << handleErrors();
return {};
}
return result;
}
-QByteArray decryptStringAsymmetric(EVP_PKEY *privateKey, const QByteArray& data) {
+namespace internals {
+
+std::optional<QByteArray> decryptStringAsymmetric(ENGINE *sslEngine,
+ EVP_PKEY *privateKey,
+ int pad_mode,
+ const QByteArray& binaryData) {
int err = -1;
- qCInfo(lcCseDecryption()) << "Start to work the decryption.";
- auto ctx = PKeyCtx::forKey(privateKey, ENGINE_get_default_RSA());
+ auto ctx = PKeyCtx::forKey(privateKey, sslEngine);
if (!ctx) {
- qCInfo(lcCseDecryption()) << "Could not create the PKEY context.";
- handleErrors();
+ qCInfo(lcCseDecryption()) << "Could not create the PKEY context." << handleErrors();
return {};
}
err = EVP_PKEY_decrypt_init(ctx);
if (err <= 0) {
- qCInfo(lcCseDecryption()) << "Could not init the decryption of the metadata";
- handleErrors();
+ qCInfo(lcCseDecryption()) << "Could not init the decryption of the metadata" << handleErrors();
return {};
}
- if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) {
- qCInfo(lcCseDecryption()) << "Error setting the encryption padding.";
- handleErrors();
+ if (EVP_PKEY_CTX_set_rsa_padding(ctx, pad_mode) <= 0) {
+ qCInfo(lcCseDecryption()) << "Error setting the encryption padding." << handleErrors();
return {};
}
- if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) {
- qCInfo(lcCseDecryption()) << "Error setting OAEP SHA 256";
- handleErrors();
+ if (pad_mode != RSA_PKCS1_PADDING && EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha1()) <= 0) {
+ qCInfo(lcCseDecryption()) << "Error setting OAEP SHA 256" << handleErrors();
return {};
}
- if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()) <= 0) {
- qCInfo(lcCseDecryption()) << "Error setting MGF1 padding";
- handleErrors();
+ if (pad_mode != RSA_PKCS1_PADDING && EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha1()) <= 0) {
+ qCInfo(lcCseDecryption()) << "Error setting MGF1 padding" << handleErrors();
return {};
}
size_t outlen = 0;
- err = EVP_PKEY_decrypt(ctx, nullptr, &outlen, (unsigned char *)data.constData(), data.size());
+ err = EVP_PKEY_decrypt(ctx, nullptr, &outlen, (unsigned char *)binaryData.constData(), binaryData.size());
if (err <= 0) {
- qCInfo(lcCseDecryption()) << "Could not determine the buffer length";
- handleErrors();
+ qCInfo(lcCseDecryption()) << "Could not determine the buffer length" << handleErrors();
return {};
- } else {
- qCInfo(lcCseDecryption()) << "Size of output is: " << outlen;
- qCInfo(lcCseDecryption()) << "Size of data is: " << data.size();
}
QByteArray out(static_cast<int>(outlen), '\0');
- if (EVP_PKEY_decrypt(ctx, unsignedData(out), &outlen, (unsigned char *)data.constData(), data.size()) <= 0) {
+ if (EVP_PKEY_decrypt(ctx, unsignedData(out), &outlen, (unsigned char *)binaryData.constData(), binaryData.size()) <= 0) {
const auto error = handleErrors();
qCCritical(lcCseDecryption()) << "Could not decrypt the data." << error;
return {};
- } else {
- qCInfo(lcCseDecryption()) << "data decrypted successfully";
}
// we don't need extra zeroes in out, so let's only return meaningful data
out = QByteArray(out.constData(), outlen);
-
- qCInfo(lcCse()) << out;
- return out;
+ return out.toBase64();
}
-QByteArray encryptStringAsymmetric(EVP_PKEY *publicKey, const QByteArray& data) {
- int err = -1;
-
- auto ctx = PKeyCtx::forKey(publicKey, ENGINE_get_default_RSA());
+std::optional<QByteArray> encryptStringAsymmetric(ENGINE *sslEngine,
+ EVP_PKEY *publicKey,
+ int pad_mode,
+ const QByteArray& binaryData) {
+ auto ctx = PKeyCtx::forKey(publicKey, sslEngine);
if (!ctx) {
- qCInfo(lcCse()) << "Could not initialize the pkey context.";
- exit(1);
+ qCInfo(lcCseEncryption()) << "Could not initialize the pkey context." << publicKey << sslEngine;
+ return {};
}
if (EVP_PKEY_encrypt_init(ctx) != 1) {
- qCInfo(lcCse()) << "Error initilaizing the encryption.";
- exit(1);
+ qCInfo(lcCseEncryption()) << "Error initilaizing the encryption." << handleErrors();
+ return {};
}
- if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) {
- qCInfo(lcCse()) << "Error setting the encryption padding.";
- exit(1);
+ if (EVP_PKEY_CTX_set_rsa_padding(ctx, pad_mode) <= 0) {
+ qCInfo(lcCseEncryption()) << "Error setting the encryption padding." << handleErrors();
+ return {};
}
- if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) {
- qCInfo(lcCse()) << "Error setting OAEP SHA 256";
- exit(1);
+ if (pad_mode != RSA_PKCS1_PADDING && EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha1()) <= 0) {
+ qCInfo(lcCseEncryption()) << "Error setting OAEP SHA 256" << handleErrors();
+ return {};
}
- if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()) <= 0) {
- qCInfo(lcCse()) << "Error setting MGF1 padding";
- exit(1);
+ if (pad_mode != RSA_PKCS1_PADDING && EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha1()) <= 0) {
+ qCInfo(lcCseEncryption()) << "Error setting MGF1 padding" << handleErrors();
+ return {};
}
size_t outLen = 0;
- if (EVP_PKEY_encrypt(ctx, nullptr, &outLen, (unsigned char *)data.constData(), data.size()) != 1) {
- qCInfo(lcCse()) << "Error retrieving the size of the encrypted data";
- exit(1);
- } else {
- qCInfo(lcCse()) << "Encryption Length:" << outLen;
+ if (EVP_PKEY_encrypt(ctx, nullptr, &outLen, (unsigned char *)binaryData.constData(), binaryData.size()) != 1) {
+ qCInfo(lcCseEncryption()) << "Error retrieving the size of the encrypted data" << handleErrors();
+ return {};
}
QByteArray out(static_cast<int>(outLen), '\0');
- if (EVP_PKEY_encrypt(ctx, unsignedData(out), &outLen, (unsigned char *)data.constData(), data.size()) != 1) {
- qCInfo(lcCse()) << "Could not encrypt key." << err;
- exit(1);
+ if (EVP_PKEY_encrypt(ctx, unsignedData(out), &outLen, (unsigned char *)binaryData.constData(), binaryData.size()) != 1) {
+ qCInfo(lcCseEncryption()) << "Could not encrypt key." << handleErrors();
+ return {};
}
- qCInfo(lcCse()) << out.toBase64();
- return out;
+ // Transform the encrypted data into base64.
+ return out.toBase64();
+}
+
+}
+
+void debugOpenssl()
+{
+ if (ERR_peek_error() == 0) {
+ return;
+ }
+
+ const char *file;
+ char errorMessage[255];
+ int line;
+ while (const auto errorNumber = ERR_get_error_line(&file, &line)) {
+ ERR_error_string(errorNumber, errorMessage);
+ qCWarning(lcCse()) << errorMessage << file << line;
+ }
+}
+
+}
+
+
+ClientSideEncryption::ClientSideEncryption()
+{
}
+bool ClientSideEncryption::isInitialized() const
+{
+ return useTokenBasedEncryption() || !getMnemonic().isEmpty();
}
-ClientSideEncryption::ClientSideEncryption() = default;
+QSslKey ClientSideEncryption::getPublicKey() const
+{
+ return _encryptionCertificate.getSslPublicKey();
+}
-void ClientSideEncryption::initialize(const AccountPtr &account)
+const QByteArray &ClientSideEncryption::getPrivateKey() const
+{
+ return _encryptionCertificate.getPrivateKeyData();
+}
+
+void ClientSideEncryption::setPrivateKey(const QByteArray &privateKey)
+{
+ _encryptionCertificate.setPrivateKeyData(privateKey);
+}
+
+const CertificateInformation &ClientSideEncryption::getCertificateInformation() const
+{
+ return _encryptionCertificate;
+}
+
+CertificateInformation ClientSideEncryption::getCertificateInformationByFingerprint(const QByteArray &certificateFingerprint) const
+{
+ CertificateInformation result;
+
+ if (_encryptionCertificate.sha256Fingerprint() == certificateFingerprint) {
+ result = _encryptionCertificate;
+ } else {
+ for(const auto &oneCertificate : _otherCertificates) {
+ if (oneCertificate.sha256Fingerprint() == certificateFingerprint) {
+ result = oneCertificate;
+ break;
+ }
+ }
+ }
+
+ return result;
+}
+
+int ClientSideEncryption::paddingMode() const
+{
+ return RSA_PKCS1_PADDING;
+}
+
+CertificateInformation ClientSideEncryption::getTokenCertificateByFingerprint(const QByteArray &expectedFingerprint) const
+{
+ CertificateInformation result;
+
+ if (_encryptionCertificate.sha256Fingerprint() == expectedFingerprint) {
+ result = _encryptionCertificate;
+ return result;
+ }
+
+ const auto itCertificate = std::find_if(_otherCertificates.begin(), _otherCertificates.end(), [expectedFingerprint] (const auto &oneCertificate) {
+ return oneCertificate.sha256Fingerprint() == expectedFingerprint;
+ });
+ if (itCertificate != _otherCertificates.end()) {
+ result = *itCertificate;
+ return result;
+ }
+
+ return result;
+}
+
+bool ClientSideEncryption::useTokenBasedEncryption() const
+{
+ return _encryptionCertificate.getPkcs11PrivateKey();
+}
+
+const QString &ClientSideEncryption::getMnemonic() const
+{
+ return _mnemonic;
+}
+
+void ClientSideEncryption::setCertificate(const QSslCertificate &certificate)
+{
+ _encryptionCertificate = CertificateInformation{_encryptionCertificate.getPrivateKeyData(), QSslCertificate{certificate}};
+}
+
+const QSslCertificate& ClientSideEncryption::getCertificate() const
+{
+ return _encryptionCertificate.getCertificate();
+}
+
+ENGINE* ClientSideEncryption::sslEngine() const
+{
+ return ENGINE_get_default_RSA();
+}
+
+ClientSideEncryptionTokenSelector *ClientSideEncryption::usbTokenInformation()
+{
+ return &_usbTokenInformation;
+}
+
+bool ClientSideEncryption::canEncrypt() const
+{
+ if (!isInitialized()) {
+ return false;
+ }
+ if (useTokenBasedEncryption()) {
+ return _encryptionCertificate.canEncrypt();
+ }
+
+ return true;
+}
+
+bool ClientSideEncryption::canDecrypt() const
+{
+ return isInitialized();
+}
+
+bool ClientSideEncryption::userCertificateNeedsMigration() const
+{
+ if (!isInitialized()) {
+ return false;
+ }
+ if (useTokenBasedEncryption()) {
+ return _encryptionCertificate.userCertificateNeedsMigration();
+ }
+
+ return false;
+}
+
+QByteArray ClientSideEncryption::certificateSha256Fingerprint() const
+{
+ if (useTokenBasedEncryption()) {
+ return _encryptionCertificate.sha256Fingerprint();
+ }
+
+ return {};
+}
+
+void ClientSideEncryption::initialize(QWidget *settingsDialog,
+ const AccountPtr &account)
{
Q_ASSERT(account);
return;
}
- fetchCertificateFromKeyChain(account);
+ if (account->enforceUseHardwareTokenEncryption()) {
+ addExtraRootCertificates();
+ if (_usbTokenInformation.isSetup()) {
+ initializeHardwareTokenEncryption(settingsDialog, account);
+ } else if (account->e2eEncryptionKeysGenerationAllowed() && account->askUserForMnemonic()) {
+ Q_EMIT startingDiscoveryEncryptionUsbToken();
+ auto futureTokenDiscoveryResult = new QFutureWatcher<void>(this);
+ auto tokenDiscoveryResult = _usbTokenInformation.searchForCertificates(account);
+ futureTokenDiscoveryResult->setFuture(tokenDiscoveryResult);
+ connect(futureTokenDiscoveryResult, &QFutureWatcher<void>::finished,
+ this, [this, settingsDialog, account, futureTokenDiscoveryResult] () {
+ completeHardwareTokenInitialization(settingsDialog, account);
+ futureTokenDiscoveryResult->deleteLater();
+ Q_EMIT finishedDiscoveryEncryptionUsbToken();
+ });
+ } else {
+ emit initializationFinished();
+ }
+ } else {
+ fetchCertificateFromKeyChain(account);
+ }
+}
+
+void ClientSideEncryption::addExtraRootCertificates()
+{
+#if defined(Q_OS_WIN)
+ auto sslConfig = QSslConfiguration::defaultConfiguration();
+
+ for (const auto &storeName : std::vector<std::wstring>{L"CA"}) {
+ auto systemStore = CertOpenSystemStore(0, storeName.data());
+ if (systemStore) {
+ auto certificatePointer = PCCERT_CONTEXT{nullptr};
+ while (true) {
+ certificatePointer = CertFindCertificateInStore(systemStore, X509_ASN_ENCODING, 0, CERT_FIND_ANY, nullptr, certificatePointer);
+ if (!certificatePointer) {
+ break;
+ }
+ const auto der = QByteArray{reinterpret_cast<const char *>(certificatePointer->pbCertEncoded),
+ static_cast<int>(certificatePointer->cbCertEncoded)};
+ const auto cert = QSslCertificate{der, QSsl::Der};
+
+ qCDebug(lcCse()) << "found certificate" << cert.subjectDisplayName() << cert.issuerDisplayName() << "from store" << storeName;
+
+ sslConfig.addCaCertificate(cert);
+ }
+ CertCloseStore(systemStore, 0);
+ }
+ }
+
+ QSslConfiguration::setDefaultConfiguration(sslConfig);
+#endif
+
+ qCDebug(lcCse()) << "existing CA certificates";
+ const auto currentSslConfig = QSslConfiguration::defaultConfiguration();
+ const auto &caCertificates = currentSslConfig.caCertificates();
+ for (const auto &oneCaCertificate : caCertificates) {
+ qCDebug(lcCse()) << oneCaCertificate.subjectDisplayName() << oneCaCertificate.issuerDisplayName();
+ }
+}
+
+void ClientSideEncryption::initializeHardwareTokenEncryption(QWidget *settingsDialog,
+ const AccountPtr &account)
+{
+ auto ctx = Pkcs11Context{Pkcs11Context::State::CreateContext};
+ _tokenSlots.reset();
+ _encryptionCertificate.clear();
+ _otherCertificates.clear();
+ _context.clear();
+
+ if (PKCS11_CTX_load(ctx, account->encryptionHardwareTokenDriverPath().toLatin1().constData())) {
+ qCWarning(lcCse()) << "loading pkcs11 engine failed:" << ERR_reason_error_string(ERR_get_error());
+
+ failedToInitialize(account);
+ return;
+ }
+
+ auto tokensCount = 0u;
+ PKCS11_SLOT *tempTokenSlots = nullptr;
+ /* get information on all slots */
+ if (PKCS11_enumerate_slots(ctx, &tempTokenSlots, &tokensCount) < 0) {
+ qCWarning(lcCse()) << "no slots available" << ERR_reason_error_string(ERR_get_error());
+
+ failedToInitialize(account);
+ return;
+ }
+
+ auto deleter = [ctx = static_cast<PKCS11_CTX*>(ctx), tokensCount] (PKCS11_SLOT* pointer) noexcept -> void {
+ qCWarning(lcCse()) << "destructor" << pointer << ctx;
+ if (pointer) {
+ qCWarning(lcCse()) << "destructor" << pointer << ctx;
+ PKCS11_release_all_slots(ctx, pointer, tokensCount);
+ }
+ };
+
+ auto tokenSlots = decltype(_tokenSlots){tempTokenSlots, deleter};
+
+ auto currentSlot = static_cast<PKCS11_SLOT*>(nullptr);
+ for(auto i = 0u; i < tokensCount; ++i) {
+ currentSlot = PKCS11_find_next_token(ctx, tokenSlots.get(), tokensCount, currentSlot);
+ if (currentSlot == nullptr || currentSlot->token == nullptr) {
+ break;
+ }
+
+ qCDebug(lcCse()) << "Slot manufacturer......:" << currentSlot->manufacturer;
+ qCDebug(lcCse()) << "Slot description.......:" << currentSlot->description;
+ qCDebug(lcCse()) << "Slot token label.......:" << currentSlot->token->label;
+ qCDebug(lcCse()) << "Slot token manufacturer:" << currentSlot->token->manufacturer;
+ qCDebug(lcCse()) << "Slot token model.......:" << currentSlot->token->model;
+ qCDebug(lcCse()) << "Slot token serialnr....:" << currentSlot->token->serialnr;
+
+ if (PKCS11_open_session(currentSlot, 0) != 0) {
+ qCWarning(lcCse()) << "PKCS11_open_session failed" << ERR_reason_error_string(ERR_get_error());
+
+ failedToInitialize(account);
+ return;
+ }
+
+ auto logged_in = 0;
+ if (PKCS11_is_logged_in(currentSlot, 0, &logged_in) != 0) {
+ qCWarning(lcCse()) << "PKCS11_is_logged_in failed" << ERR_reason_error_string(ERR_get_error());
+
+ failedToInitialize(account);
+ return;
+ }
+
+ while (true) {
+ auto pinHasToBeCached = false;
+ auto newPin = _cachedPin;
+
+ if (newPin.isEmpty()) {
+ /* perform pkcs #11 login */
+ bool ok;
+ newPin = QInputDialog::getText(settingsDialog,
+ tr("PIN needed to login to token"),
+ tr("Enter Certificate USB Token PIN:"),
+ QLineEdit::Password,
+ {},
+ &ok);
+ if (!ok || newPin.isEmpty()) {
+ qCWarning(lcCse()) << "an USER pin is required";
+
+ Q_EMIT initializationFinished();
+ return;
+ }
+
+ pinHasToBeCached = true;
+ }
+
+ const auto newPinData = newPin.toLatin1();
+ if (PKCS11_login(currentSlot, 0, newPinData.data()) != 0) {
+ QMessageBox::warning(settingsDialog,
+ tr("Invalid PIN. Login failed"),
+ tr("Login to the token failed after providing the user PIN. It may be invalid or wrong. Please try again !"),
+ QMessageBox::Ok);
+ _cachedPin.clear();
+ continue;
+ }
+
+ /* check if user is logged in */
+ if (PKCS11_is_logged_in(currentSlot, 0, &logged_in) != 0) {
+ qCWarning(lcCse()) << "PKCS11_is_logged_in failed" << ERR_reason_error_string(ERR_get_error());
+
+ _cachedPin.clear();
+ failedToInitialize(account);
+ return;
+ }
+ if (!logged_in) {
+ qCWarning(lcCse()) << "PKCS11_is_logged_in says user is not logged in, expected to be logged in";
+
+ _cachedPin.clear();
+ failedToInitialize(account);
+ return;
+ }
+
+ if (pinHasToBeCached) {
+ cacheTokenPin(newPin);
+ }
+
+ break;
+ }
+
+ auto keysCount = 0u;
+ auto certificatesFromToken = static_cast<PKCS11_CERT*>(nullptr);
+ if (PKCS11_enumerate_certs(currentSlot->token, &certificatesFromToken, &keysCount)) {
+ qCWarning(lcCse()) << "PKCS11_enumerate_certs failed" << ERR_reason_error_string(ERR_get_error());
+
+ failedToInitialize(account);
+ return;
+ }
+
+ for (auto certificateIndex = 0u; certificateIndex < keysCount; ++certificateIndex) {
+ const auto currentCertificate = &certificatesFromToken[certificateIndex];
+
+ Bio out;
+ const auto ret = PEM_write_bio_X509(out, currentCertificate->x509);
+ if (ret <= 0){
+ qCWarning(lcCse()) << "PEM_write_bio_X509 failed" << ERR_reason_error_string(ERR_get_error());
+
+ failedToInitialize(account);
+ return;
+ }
+
+ const auto result = BIO2ByteArray(out);
+ auto sslCertificate = QSslCertificate{result, QSsl::Pem};
+
+ if (sslCertificate.isSelfSigned()) {
+ qCDebug(lcCse()) << "newly found certificate is self signed: goint to ignore it";
+ continue;
+ }
+
+ const auto certificateKey = PKCS11_find_key(currentCertificate);
+ if (!certificateKey) {
+ qCWarning(lcCse()) << "PKCS11_find_key failed" << ERR_reason_error_string(ERR_get_error());
+
+ failedToInitialize(account);
+ return;
+ }
+
+ qCDebug(lcCse) << "checking the type of the key associated to the certificate";
+ qCDebug(lcCse) << "key type" << Qt::hex << PKCS11_get_key_type(certificateKey);
+
+ _otherCertificates.emplace_back(certificateKey, std::move(sslCertificate));
+ }
+ }
+
+ for (const auto &oneCertificateInformation : _otherCertificates) {
+ if (oneCertificateInformation.isSelfSigned()) {
+ qCDebug(lcCse()) << "newly found certificate is self signed: goint to ignore it";
+ continue;
+ }
+
+ if (!_usbTokenInformation.sha256Fingerprint().isEmpty() && oneCertificateInformation.sha256Fingerprint() != _usbTokenInformation.sha256Fingerprint()) {
+ qCDebug(lcCse()) << "skipping certificate from" << "with fingerprint" << oneCertificateInformation.sha256Fingerprint() << "different from" << _usbTokenInformation.sha256Fingerprint();
+ continue;
+ }
+
+ const auto &sslErrors = oneCertificateInformation.verify();
+ for (const auto &sslError : sslErrors) {
+ qCInfo(lcCse()) << "certificate validation error" << sslError;
+ }
+
+ setEncryptionCertificate(oneCertificateInformation);
+
+ if (canEncrypt() && !checkEncryptionIsWorking()) {
+ qCWarning(lcCse()) << "encryption is not properly setup";
+
+ failedToInitialize(account);
+ return;
+ }
+
+ sendPublicKey(account);
+
+ _tokenSlots = std::move(tokenSlots);
+ _context = std::move(ctx);
+
+ return;
+ }
+
+ failedToInitialize(account);
}
void ClientSideEncryption::fetchCertificateFromKeyChain(const AccountPtr &account)
QByteArray data = EncryptionHelper::generateRandom(64);
Bio publicKeyBio;
- QByteArray publicKeyPem = account->e2e()->_publicKey.toPem();
+ QByteArray publicKeyPem = account->e2e()->getPublicKey().toPem();
BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size());
auto publicKey = PKey::readPublicKey(publicKeyBio);
- auto encryptedData = EncryptionHelper::encryptStringAsymmetric(publicKey, data.toBase64());
+ auto encryptedData = EncryptionHelper::encryptStringAsymmetric(account->e2e()->getCertificateInformation(), account->e2e()->paddingMode(), *account->e2e(), data.toBase64());
+ if (!encryptedData) {
+ qCWarning(lcCse()) << "encryption error";
+ return false;
+ }
- Bio privateKeyBio;
- QByteArray privateKeyPem = account->e2e()->_privateKey;
- BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size());
- auto key = PKey::readPrivateKey(privateKeyBio);
+ auto key = _encryptionCertificate.getEvpPrivateKey();
- QByteArray decryptResult = QByteArray::fromBase64(EncryptionHelper::decryptStringAsymmetric(key, encryptedData));
+ const auto decryptionResult = EncryptionHelper::decryptStringAsymmetric(account->e2e()->getCertificateInformation(), account->e2e()->paddingMode(), *account->e2e(), *encryptedData);
+ if (!decryptionResult) {
+ qCWarning(lcCse()) << "encryption error";
+ return false;
+ }
+ const auto decryptResult = QByteArray::fromBase64(*decryptionResult);
if (data != decryptResult) {
qCInfo(lcCse()) << "invalid private key";
return true;
}
+bool ClientSideEncryption::checkEncryptionIsWorking() const
+{
+ qCInfo(lcCse) << "check encryption is working before enabling end-to-end encryption feature";
+ QByteArray data = EncryptionHelper::generateRandom(64);
+
+ auto encryptedData = EncryptionHelper::encryptStringAsymmetric(getCertificateInformation(), paddingMode(), *this, data);
+ if (!encryptedData) {
+ qCWarning(lcCse()) << "encryption error";
+ return false;
+ }
+
+ qCDebug(lcCse) << "encryption is working with" << getCertificateInformation().sha256Fingerprint();
+
+ const auto decryptionResult = EncryptionHelper::decryptStringAsymmetric(getCertificateInformation(), paddingMode(), *this, *encryptedData);
+ if (!decryptionResult) {
+ qCWarning(lcCse()) << "encryption error";
+ return false;
+ }
+
+ qCDebug(lcCse) << "decryption is working with" << getCertificateInformation().sha256Fingerprint();
+
+ QByteArray decryptResult = QByteArray::fromBase64(*decryptionResult);
+
+ if (data != decryptResult) {
+ qCInfo(lcCse()) << "recovered data does not match the initial data after encryption and decryption of it";
+ return false;
+ }
+
+ qCInfo(lcCse) << "end-to-end encryption is working with" << getCertificateInformation().sha256Fingerprint();
+
+ return true;
+}
+
bool ClientSideEncryption::checkServerPublicKeyValidity(const QByteArray &serverPublicKeyString) const
{
Bio serverPublicKeyBio;
const auto serverPublicKey = PKey::readPrivateKey(serverPublicKeyBio);
Bio certificateBio;
- const auto certificatePem = _certificate.toPem();
+ const auto certificatePem = _encryptionCertificate.getCertificate().toPem();
BIO_write(certificateBio, certificatePem.constData(), certificatePem.size());
const auto x509Certificate = X509Certificate::readCertificate(certificateBio);
if (!x509Certificate) {
return;
}
- _certificate = QSslCertificate(readJob->binaryData(), QSsl::Pem);
+ _encryptionCertificate = CertificateInformation{_encryptionCertificate.getPrivateKeyData(), QSslCertificate{readJob->binaryData(), QSsl::Pem}};
- if (_certificate.isNull()) {
+ if (_encryptionCertificate.getCertificate().isNull()) {
fetchPublicKeyFromKeyChain(account);
return;
}
- _publicKey = _certificate.publicKey();
-
qCInfo(lcCse()) << "Public key fetched from keychain";
const QString kck = AbstractCredentials::keychainKey(
QByteArray ClientSideEncryption::generateSignatureCryptographicMessageSyntax(const QByteArray &data) const
{
Bio certificateBio;
- const auto certificatePem = _certificate.toPem();
+ const auto certificatePem = _encryptionCertificate.getCertificate().toPem();
BIO_write(certificateBio, certificatePem.constData(), certificatePem.size());
const auto x509Certificate = X509Certificate::readCertificate(certificateBio);
if (!x509Certificate) {
return {};
}
- Bio privateKeyBio;
- BIO_write(privateKeyBio, _privateKey.constData(), _privateKey.size());
- const auto privateKey = PKey::readPrivateKey(privateKeyBio);
+ const auto privateKey = _encryptionCertificate.getEvpPrivateKey();
Bio dataBio;
BIO_write(dataBio, data.constData(), data.size());
const auto contentInfo = CMS_sign(x509Certificate, privateKey, nullptr, dataBio, CMS_DETACHED);
+ Q_ASSERT(contentInfo);
if (!contentInfo) {
return {};
}
return;
}
- _publicKey = publicKey;
+ Q_UNUSED(publicKey)
const QString kck = AbstractCredentials::keychainKey(
account->url().toString(),
return;
}
- //_privateKey = QSslKey(readJob->binaryData(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
- _privateKey = readJob->binaryData();
+ _encryptionCertificate.setPrivateKeyData(readJob->binaryData());
- if (_privateKey.isNull()) {
+ if (getPrivateKey().isNull()) {
getPrivateKeyFromServer(account);
return;
}
return;
}
- _mnemonic = readJob->textData();
+ setMnemonic(readJob->textData());
- qCInfo(lcCse()) << "Mnemonic key fetched from keychain: " << _mnemonic;
+ qCInfo(lcCse()) << "Mnemonic key fetched from keychain";
checkServerHasSavedKeys(account);
}
auto *job = new WritePasswordJob(Theme::instance()->appName());
job->setInsecureFallback(false);
job->setKey(kck);
- job->setBinaryData(_privateKey);
+ job->setBinaryData(getPrivateKey());
connect(job, &WritePasswordJob::finished, [](Job *incoming) {
Q_UNUSED(incoming);
qCInfo(lcCse()) << "Private key stored in keychain";
auto *job = new WritePasswordJob(Theme::instance()->appName());
job->setInsecureFallback(false);
job->setKey(kck);
- job->setBinaryData(_certificate.toPem());
+ job->setBinaryData(_encryptionCertificate.getCertificate().toPem());
connect(job, &WritePasswordJob::finished, [](Job *incoming) {
Q_UNUSED(incoming);
qCInfo(lcCse()) << "Certificate stored in keychain";
job->start();
}
+void ClientSideEncryption::completeHardwareTokenInitialization(QWidget *settingsDialog,
+ const OCC::AccountPtr &account)
+{
+ if (_usbTokenInformation.isSetup()) {
+ initializeHardwareTokenEncryption(settingsDialog, account);
+ } else {
+ emit initializationFinished();
+ }
+}
+
+void ClientSideEncryption::setMnemonic(const QString &mnemonic)
+{
+ if (_mnemonic == mnemonic) {
+ return;
+ }
+
+ _mnemonic = mnemonic;
+
+ Q_EMIT canEncryptChanged();
+ Q_EMIT canDecryptChanged();
+}
+
+void ClientSideEncryption::setEncryptionCertificate(CertificateInformation certificateInfo)
+{
+ if (_encryptionCertificate == certificateInfo) {
+ return;
+ }
+
+ const auto oldValueForUserCertificateNeedsMigration = _encryptionCertificate.userCertificateNeedsMigration();
+
+ _encryptionCertificate = std::move(certificateInfo);
+
+ Q_EMIT canEncryptChanged();
+ Q_EMIT canDecryptChanged();
+
+ if (oldValueForUserCertificateNeedsMigration != _encryptionCertificate.userCertificateNeedsMigration()) {
+ Q_EMIT userCertificateNeedsMigrationChanged();
+ }
+}
+
void ClientSideEncryption::generateMnemonic()
{
const auto list = WordList::getRandomWords(12);
- _mnemonic = list.join(' ');
+ setMnemonic(list.join(' '));
}
template <typename L>
return job;
};
+ if (!account->credentials()) {
+ return;
+ }
+
const auto user = account->credentials()->user();
const auto deletePrivateKeyJob = createDeleteJob(user + e2e_private);
const auto deleteCertJob = createDeleteJob(user + e2e_cert);
deletePrivateKeyJob->start();
deleteCertJob->start();
deleteMnemonicJob->start();
+ _usbTokenInformation.setSha256Fingerprint({});
+ account->setEncryptionCertificateFingerprint({});
+ _tokenSlots.reset();
+ _encryptionCertificate.clear();
+ _otherCertificates.clear();
+ _context.clear();
+ Q_EMIT canDecryptChanged();
+ Q_EMIT canEncryptChanged();
+ Q_EMIT userCertificateNeedsMigrationChanged();
}
void ClientSideEncryption::getUsersPublicKeyFromServer(const AccountPtr &account, const QStringList &userIds)
job->start();
}
+void ClientSideEncryption::migrateCertificate()
+{
+ _usbTokenInformation.clear();
+}
+
void ClientSideEncryption::handlePrivateKeyDeleted(const QKeychain::Job* const incoming)
{
const auto error = incoming->error();
}
qCDebug(lcCse) << "Private key successfully deleted from keychain. Clearing.";
- _privateKey = QByteArray();
+ _encryptionCertificate.clear();
+
Q_EMIT privateKeyDeleted();
checkAllSensitiveDataDeleted();
}
}
qCDebug(lcCse) << "Certificate successfully deleted from keychain. Clearing.";
- _certificate = QSslCertificate();
+ _encryptionCertificate.clear();
Q_EMIT certificateDeleted();
checkAllSensitiveDataDeleted();
}
}
qCDebug(lcCse) << "Mnemonic successfully deleted from keychain. Clearing.";
- _mnemonic = QString();
+ setMnemonic({});
Q_EMIT mnemonicDeleted();
checkAllSensitiveDataDeleted();
}
return;
}
- _publicKey.clear();
Q_EMIT publicKeyDeleted();
checkAllSensitiveDataDeleted();
}
bool ClientSideEncryption::sensitiveDataRemaining() const
{
- return !_privateKey.isEmpty() || !_certificate.isNull() || !_mnemonic.isEmpty();
+ return !getPrivateKey().isEmpty() || !_encryptionCertificate.getCertificate().isNull() || !_mnemonic.isEmpty() || !_usbTokenInformation.sha256Fingerprint().isEmpty() || _encryptionCertificate.sensitiveDataRemaining();
}
void ClientSideEncryption::failedToInitialize(const AccountPtr &account)
Q_EMIT initializationFinished();
}
+void ClientSideEncryption::saveCertificateIdentification(const AccountPtr &account) const
+{
+ account->setEncryptionCertificateFingerprint(_usbTokenInformation.sha256Fingerprint());
+}
+
+void ClientSideEncryption::cacheTokenPin(const QString pin)
+{
+ _cachedPin = pin;
+ QTimer::singleShot(86400000, [this] () {
+ _cachedPin.clear();
+ });
+}
+
void ClientSideEncryption::checkAllSensitiveDataDeleted()
{
if (sensitiveDataRemaining()) {
qCWarning(lcCse) << "Some sensitive data emaining:"
- << "Private key:" << (_privateKey.isEmpty() ? "is empty" : "is not empty")
- << "Certificate is null:" << (_certificate.isNull() ? "true" : "false")
+ << "Private key:" << (getPrivateKey().isEmpty() ? "is empty" : "is not empty")
+ << "Certificate is null:" << (_encryptionCertificate.getCertificate().isNull() ? "true" : "false")
<< "Mnemonic:" << (_mnemonic.isEmpty() ? "is empty" : "is not empty");
return;
}
return;
}
- _privateKey = BIO2ByteArray(privKey);
+ _encryptionCertificate.setPrivateKeyData(BIO2ByteArray(privKey));
}
Bio privKey;
auto job = new SignPublicKeyApiJob(account, e2eeBaseUrl(account) + "public-key", this);
job->setCsr(csrContent);
- connect(job, &SignPublicKeyApiJob::jsonReceived, [this, account, keyPair = std::move(keyPair)](const QJsonDocument& json, const int retCode) {
+ connect(job, &SignPublicKeyApiJob::jsonReceived, job, [this, account, keyPair = std::move(keyPair)](const QJsonDocument& json, const int retCode) {
if (retCode == 200) {
const auto cert = json.object().value("ocs").toObject().value("data").toObject().value("public-key").toString();
- _certificate = QSslCertificate(cert.toLocal8Bit(), QSsl::Pem);
- _publicKey = _certificate.publicKey();
+ _encryptionCertificate = CertificateInformation{_encryptionCertificate.getPrivateKeyData(), QSslCertificate{cert.toLocal8Bit(), QSsl::Pem}};
Bio certificateBio;
- const auto certificatePem = _certificate.toPem();
+ const auto certificatePem = _encryptionCertificate.getCertificate().toPem();
BIO_write(certificateBio, certificatePem.constData(), certificatePem.size());
const auto x509Certificate = X509Certificate::readCertificate(certificateBio);
if (!X509_check_private_key(x509Certificate, keyPair)) {
job->start();
}
+void ClientSideEncryption::sendPublicKey(const AccountPtr &account)
+{
+ // Send public key to the server
+ auto job = new StorePublicKeyApiJob(account, e2eeBaseUrl(account) + "public-key", this);
+ job->setPublicKey(_encryptionCertificate.getCertificate().toPem());
+ connect(job, &StorePublicKeyApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) {
+ Q_UNUSED(doc);
+ switch(retCode) {
+ case 200:
+ case 409:
+ saveCertificateIdentification(account);
+ emit initializationFinished();
+
+ break;
+ default:
+ qCWarning(lcCse) << "Store certificate failed, return code:" << retCode;
+ failedToInitialize(account);
+ }
+ });
+ job->start();
+}
+
void ClientSideEncryption::writeKeyPair(const AccountPtr &account,
PKey keyPair,
const QByteArray &csrContent)
auto salt = EncryptionHelper::generateRandom(40);
auto secretKey = EncryptionHelper::generatePassword(passPhrase, salt);
- auto cryptedText = EncryptionHelper::encryptPrivateKey(secretKey, EncryptionHelper::privateKeyToPem(_privateKey), salt);
+ auto cryptedText = EncryptionHelper::encryptPrivateKey(secretKey, EncryptionHelper::privateKeyToPem(getPrivateKey()), salt);
// Send private key to the server
auto job = new StorePrivateKeyApiJob(account, e2eeBaseUrl(account) + "private-key", this);
if (ok) {
prev = dialog.textValue();
- _mnemonic = prev;
+ setMnemonic(prev);
QString mnemonic = prev.split(" ").join(QString()).toLower();
// split off salt
const auto privateKey = EncryptionHelper::decryptPrivateKey(password, key);
if (!privateKey.isEmpty()) {
- _privateKey = privateKey;
+ _encryptionCertificate.setPrivateKeyData(privateKey);
} else {
const auto deprecatedSha1PrivateKey = EncryptionHelper::decryptPrivateKey(deprecatedSha1Password, key);
if (!deprecatedSha1PrivateKey.isEmpty()) {
- _privateKey = deprecatedSha1PrivateKey;
+ _encryptionCertificate.setPrivateKeyData(deprecatedSha1PrivateKey);
} else {
- _privateKey = EncryptionHelper::decryptPrivateKey(deprecatedPassword, key);
+ _encryptionCertificate.setPrivateKeyData(EncryptionHelper::decryptPrivateKey(deprecatedPassword, key));
}
}
- if (!_privateKey.isNull() && checkPublicKeyValidity(account)) {
+ if (!getPrivateKey().isNull() && checkPublicKeyValidity(account)) {
writePrivateKey(account);
writeCertificate(account);
writeMnemonic(account, [] () {});
connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) {
if (retCode == 200) {
QString publicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-keys"].toObject()[account->davUser()].toString();
- _certificate = QSslCertificate(publicKey.toLocal8Bit(), QSsl::Pem);
- _publicKey = _certificate.publicKey();
+ _encryptionCertificate = CertificateInformation{_encryptionCertificate.getPrivateKeyData(), QSslCertificate{publicKey.toLocal8Bit(), QSsl::Pem}};
fetchAndValidatePublicKeyFromServer(account);
} else if (retCode == 404) {
qCDebug(lcCse()) << "No public key on the server";
if (retCode == 200) {
const auto serverPublicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-key"].toString().toLatin1();
if (checkServerPublicKeyValidity(serverPublicKey)) {
- if (_privateKey.isEmpty()) {
+ if (getPrivateKey().isEmpty()) {
getPrivateKeyFromServer(account);
} else {
encryptPrivateKey(account);
return _certificate;
}
+CertificateInformation::CertificateInformation()
+{
+ checkEncryptionCertificate();
+}
+
+CertificateInformation::CertificateInformation(PKCS11_KEY *hardwarePrivateKey,
+ QSslCertificate &&certificate)
+ : _hardwarePrivateKey(hardwarePrivateKey)
+ , _certificate(std::move(certificate))
+{
+ checkEncryptionCertificate();
+}
+
+CertificateInformation::CertificateInformation(const QByteArray &privateKey, QSslCertificate &&certificate)
+ : _hardwarePrivateKey()
+ , _privateKeyData()
+ , _certificate(std::move(certificate))
+{
+ if (!privateKey.isEmpty()) {
+ setPrivateKeyData(privateKey);
+ }
+
+ checkEncryptionCertificate();
+}
+
+bool CertificateInformation::operator==(const CertificateInformation &other) const
+{
+ return _certificate.digest(QCryptographicHash::Sha256) == other._certificate.digest(QCryptographicHash::Sha256);
+}
+
+void CertificateInformation::clear()
+{
+ _hardwarePrivateKey = nullptr;
+ _privateKeyData.clear();
+ _certificate.clear();
+ _certificateExpired = true;
+ _certificateNotYetValid = true;
+ _certificateRevoked = true;
+ _certificateInvalid = true;
+}
+
+const QByteArray& CertificateInformation::getPrivateKeyData() const
+{
+ return _privateKeyData;
+}
+
+void CertificateInformation::setPrivateKeyData(const QByteArray &privateKey)
+{
+ _privateKeyData = privateKey;
+}
+
+QList<QSslError> CertificateInformation::verify() const
+{
+ auto result = QSslCertificate::verify({_certificate});
+
+ auto hasNeededExtendedKeyUsageExtension = false;
+ for (const auto &oneExtension : _certificate.extensions()) {
+ if (oneExtension.oid() == QStringLiteral("2.5.29.37")) {
+ const auto extendedKeyUsageList = oneExtension.value().toList();
+ for (const auto &oneExtendedKeyUsageValue : extendedKeyUsageList) {
+ if (oneExtendedKeyUsageValue == QStringLiteral("E-mail Protection")) {
+ hasNeededExtendedKeyUsageExtension = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!hasNeededExtendedKeyUsageExtension) {
+ result.append(QSslError{QSslError::InvalidPurpose});
+ }
+
+ return result;
+}
+
+bool CertificateInformation::isSelfSigned() const
+{
+ return _certificate.isSelfSigned();
+}
+
+QSslKey CertificateInformation::getSslPublicKey() const
+{
+ return _certificate.publicKey();
+}
+
+PKey CertificateInformation::getEvpPublicKey() const
+{
+ const auto publicKey = _certificate.publicKey();
+ Q_ASSERT(!publicKey.isNull());
+ if (publicKey.isNull()) {
+ qCDebug(lcCse) << "Public key is null. Could not encrypt.";
+ }
+ Bio publicKeyBio;
+ const auto publicKeyPem = publicKey.toPem();
+ BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size());
+ return PKey::readPublicKey(publicKeyBio);
+}
+
+PKCS11_KEY *CertificateInformation::getPkcs11PrivateKey() const
+{
+ return canDecrypt() ? _hardwarePrivateKey : nullptr;
+}
+
+PKey CertificateInformation::getEvpPrivateKey() const
+{
+ if (_hardwarePrivateKey) {
+ return PKey::readHardwarePrivateKey(_hardwarePrivateKey);
+ } else {
+ const auto privateKeyPem = _privateKeyData;
+ Q_ASSERT(!privateKeyPem.isEmpty());
+ if (privateKeyPem.isEmpty()) {
+ qCDebug(lcCse) << "Private key is empty. Could not encrypt.";
+ }
+
+ Bio privateKeyBio;
+ BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size());
+ return PKey::readPrivateKey(privateKeyBio);
+ }
+}
+
+const QSslCertificate &CertificateInformation::getCertificate() const
+{
+ return _certificate;
+}
+
+bool CertificateInformation::canEncrypt() const
+{
+ return (_hardwarePrivateKey || !_certificate.isNull()) && !_certificateExpired && !_certificateNotYetValid && !_certificateRevoked && !_certificateInvalid;
+}
+
+bool CertificateInformation::canDecrypt() const
+{
+ return _hardwarePrivateKey || !_privateKeyData.isEmpty();
+}
+
+bool CertificateInformation::userCertificateNeedsMigration() const
+{
+ return _hardwarePrivateKey &&
+ (_certificateExpired || _certificateNotYetValid || _certificateRevoked || _certificateInvalid);
+}
+
+bool CertificateInformation::sensitiveDataRemaining() const
+{
+ return _hardwarePrivateKey && !_privateKeyData.isEmpty() && !_certificate.isNull();
+}
+
+QByteArray CertificateInformation::sha256Fingerprint() const
+{
+ return _certificate.digest(QCryptographicHash::Sha256).toBase64();
+}
+
+void CertificateInformation::checkEncryptionCertificate()
+{
+ _certificateExpired = false;
+ _certificateNotYetValid = false;
+ _certificateRevoked = false;
+ _certificateInvalid = false;
+
+ const auto sslErrors = QSslCertificate::verify({_certificate});
+ for (const auto &sslError : sslErrors) {
+ qCDebug(lcCse()) << "certificate validation error" << sslError;
+ switch (sslError.error())
+ {
+ case QSslError::CertificateExpired:
+ _certificateExpired = true;
+ break;
+ case QSslError::CertificateNotYetValid:
+ _certificateNotYetValid = true;
+ break;
+ case QSslError::CertificateRevoked:
+ _certificateRevoked = true;
+ break;
+ case QSslError::UnableToGetIssuerCertificate:
+ case QSslError::UnableToDecryptCertificateSignature:
+ case QSslError::UnableToDecodeIssuerPublicKey:
+ case QSslError::CertificateSignatureFailed:
+ case QSslError::InvalidNotBeforeField:
+ case QSslError::InvalidNotAfterField:
+ case QSslError::SelfSignedCertificate:
+ case QSslError::SelfSignedCertificateInChain:
+ case QSslError::UnableToGetLocalIssuerCertificate:
+ case QSslError::UnableToVerifyFirstCertificate:
+ case QSslError::InvalidCaCertificate:
+ case QSslError::PathLengthExceeded:
+ case QSslError::InvalidPurpose:
+ case QSslError::CertificateUntrusted:
+ case QSslError::CertificateRejected:
+ case QSslError::SubjectIssuerMismatch:
+ case QSslError::AuthorityIssuerSerialNumberMismatch:
+ case QSslError::NoPeerCertificate:
+ case QSslError::HostNameMismatch:
+ case QSslError::NoSslSupport:
+ case QSslError::CertificateBlacklisted:
+ case QSslError::CertificateStatusUnknown:
+ case QSslError::OcspNoResponseFound:
+ case QSslError::OcspMalformedRequest:
+ case QSslError::OcspMalformedResponse:
+ case QSslError::OcspInternalError:
+ case QSslError::OcspTryLater:
+ case QSslError::OcspSigRequred:
+ case QSslError::OcspUnauthorized:
+ case QSslError::OcspResponseCannotBeTrusted:
+ case QSslError::OcspResponseCertIdUnknown:
+ case QSslError::OcspResponseExpired:
+ case QSslError::OcspStatusUnknown:
+ case QSslError::UnspecifiedError:
+ _certificateInvalid = true;
+ break;
+ case QSslError::NoError:
+ break;
+ }
+ }
+}
+
}
+/*
+ * Copyright © 2017, Tomaz Canabrava <tcanabrava@kde.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
#ifndef CLIENTSIDEENCRYPTION_H
#define CLIENTSIDEENCRYPTION_H
#include "clientsideencryptionprimitives.h"
#include "accountfwd.h"
+#include "networkjobs.h"
+#include "clientsideencryptiontokenselector.h"
#include <QString>
#include <QObject>
#include <QMap>
#include <QHash>
+#include <libp11.h>
+
#include <openssl/evp.h>
+#include <functional>
+#include <optional>
+
+class QWidget;
+
namespace QKeychain {
class Job;
class WritePasswordJob;
QString e2eeBaseUrl(const OCC::AccountPtr &account);
+class ClientSideEncryption;
+
+class CertificateInformation {
+public:
+ CertificateInformation();
+
+ explicit CertificateInformation(PKCS11_KEY *hardwarePrivateKey,
+ QSslCertificate &&certificate);
+
+ explicit CertificateInformation(const QByteArray& privateKey,
+ QSslCertificate &&certificate);
+
+ [[nodiscard]] bool operator==(const CertificateInformation &other) const;
+
+ void clear();
+
+ [[nodiscard]] const QByteArray& getPrivateKeyData() const;
+
+ void setPrivateKeyData(const QByteArray& privateKey);
+
+ [[nodiscard]] QList<QSslError> verify() const;
+
+ [[nodiscard]] bool isSelfSigned() const;
+
+ [[nodiscard]] QSslKey getSslPublicKey() const;
+
+ [[nodiscard]] PKey getEvpPublicKey() const;
+
+ [[nodiscard]] PKCS11_KEY* getPkcs11PrivateKey() const;
+
+ [[nodiscard]] PKey getEvpPrivateKey() const;
+
+ [[nodiscard]] const QSslCertificate& getCertificate() const;
+
+ [[nodiscard]] bool canEncrypt() const;
+
+ [[nodiscard]] bool canDecrypt() const;
+
+ [[nodiscard]] bool userCertificateNeedsMigration() const;
+
+ [[nodiscard]] bool sensitiveDataRemaining() const;
+
+ [[nodiscard]] QByteArray sha256Fingerprint() const;
+
+private:
+ void checkEncryptionCertificate();
+
+ PKCS11_KEY* _hardwarePrivateKey = nullptr;
+
+ QByteArray _privateKeyData;
+
+ QSslCertificate _certificate;
+
+ bool _certificateExpired = true;
+
+ bool _certificateNotYetValid = true;
+
+ bool _certificateRevoked = true;
+
+ bool _certificateInvalid = true;
+};
+
namespace EncryptionHelper {
- OWNCLOUDSYNC_EXPORT QByteArray generateRandomFilename();
- OWNCLOUDSYNC_EXPORT QByteArray generateRandom(int size);
- QByteArray generatePassword(const QString &wordlist, const QByteArray& salt);
- OWNCLOUDSYNC_EXPORT QByteArray encryptPrivateKey(
- const QByteArray& key,
- const QByteArray& privateKey,
- const QByteArray &salt
- );
- OWNCLOUDSYNC_EXPORT QByteArray decryptPrivateKey(
- const QByteArray& key,
- const QByteArray& data
- );
- OWNCLOUDSYNC_EXPORT QByteArray extractPrivateKeySalt(const QByteArray &data);
- OWNCLOUDSYNC_EXPORT QByteArray encryptStringSymmetric(
- const QByteArray& key,
- const QByteArray& data
- );
- OWNCLOUDSYNC_EXPORT QByteArray decryptStringSymmetric(
- const QByteArray& key,
- const QByteArray& data
- );
- OWNCLOUDSYNC_EXPORT QByteArray encryptStringAsymmetric(const QSslKey key, const QByteArray &data);
- OWNCLOUDSYNC_EXPORT QByteArray decryptStringAsymmetric(const QByteArray &privateKeyPem, const QByteArray &data);
-
- QByteArray privateKeyToPem(const QByteArray key);
-
- //TODO: change those two EVP_PKEY into QSslKey.
- QByteArray encryptStringAsymmetric(
- EVP_PKEY *publicKey,
- const QByteArray& data
- );
- QByteArray decryptStringAsymmetric(
- EVP_PKEY *privateKey,
- const QByteArray& data
- );
-
- OWNCLOUDSYNC_EXPORT bool fileEncryption(const QByteArray &key, const QByteArray &iv,
- QFile *input, QFile *output, QByteArray& returnTag);
-
- OWNCLOUDSYNC_EXPORT bool fileDecryption(const QByteArray &key, const QByteArray &iv,
- QFile *input, QFile *output);
+
+OWNCLOUDSYNC_EXPORT QByteArray generateRandomFilename();
+OWNCLOUDSYNC_EXPORT QByteArray generateRandom(int size);
+QByteArray generatePassword(const QString &wordlist, const QByteArray& salt);
+OWNCLOUDSYNC_EXPORT QByteArray encryptPrivateKey(
+ const QByteArray& key,
+ const QByteArray& privateKey,
+ const QByteArray &salt
+);
+OWNCLOUDSYNC_EXPORT QByteArray decryptPrivateKey(
+ const QByteArray& key,
+ const QByteArray& data
+);
+OWNCLOUDSYNC_EXPORT QByteArray extractPrivateKeySalt(const QByteArray &data);
+OWNCLOUDSYNC_EXPORT QByteArray encryptStringSymmetric(
+ const QByteArray& key,
+ const QByteArray& data
+);
+OWNCLOUDSYNC_EXPORT QByteArray decryptStringSymmetric(
+ const QByteArray& key,
+ const QByteArray& data
+);
+
+[[nodiscard]] OWNCLOUDSYNC_EXPORT std::optional<QByteArray> encryptStringAsymmetric(const CertificateInformation &selectedCertificate,
+ int paddingMode,
+ const ClientSideEncryption &encryptionEngine,
+ const QByteArray &binaryData);
+
+[[nodiscard]] OWNCLOUDSYNC_EXPORT std::optional<QByteArray> decryptStringAsymmetric(const CertificateInformation &selectedCertificate,
+ int paddingMode,
+ const ClientSideEncryption &encryptionEngine,
+ const QByteArray &base64Data);
+
+QByteArray privateKeyToPem(const QByteArray key);
+
+OWNCLOUDSYNC_EXPORT bool fileEncryption(const QByteArray &key, const QByteArray &iv,
+ QFile *input, QFile *output, QByteArray& returnTag);
+
+OWNCLOUDSYNC_EXPORT bool fileDecryption(const QByteArray &key, const QByteArray &iv,
+ QFile *input, QFile *output);
OWNCLOUDSYNC_EXPORT bool dataEncryption(const QByteArray &key, const QByteArray &iv, const QByteArray &input, QByteArray &output, QByteArray &returnTag);
OWNCLOUDSYNC_EXPORT bool dataDecryption(const QByteArray &key, const QByteArray &iv, const QByteArray &input, QByteArray &output);
class OWNCLOUDSYNC_EXPORT ClientSideEncryption : public QObject {
Q_OBJECT
+
+ Q_PROPERTY(bool canEncrypt READ canEncrypt NOTIFY canEncryptChanged FINAL)
+ Q_PROPERTY(bool canDecrypt READ canDecrypt NOTIFY canDecryptChanged FINAL)
+ Q_PROPERTY(bool userCertificateNeedsMigration READ userCertificateNeedsMigration NOTIFY userCertificateNeedsMigrationChanged FINAL)
public:
ClientSideEncryption();
- QByteArray _privateKey;
- QSslKey _publicKey;
- QSslCertificate _certificate;
- QString _mnemonic;
- bool _newMnemonicGenerated = false;
+ [[nodiscard]] bool isInitialized() const;
+
+ [[nodiscard]] bool tokenIsSetup() const;
+
+ [[nodiscard]] QSslKey getPublicKey() const;
+
+ [[nodiscard]] const QByteArray& getPrivateKey() const;
+
+ void setPrivateKey(const QByteArray &privateKey);
+
+ [[nodiscard]] const CertificateInformation& getCertificateInformation() const;
+
+ [[nodiscard]] CertificateInformation getCertificateInformationByFingerprint(const QByteArray &certificateFingerprint) const;
+
+ [[nodiscard]] int paddingMode() const;
+
+ [[nodiscard]] CertificateInformation getTokenCertificateByFingerprint(const QByteArray &expectedFingerprint) const;
+
+ [[nodiscard]] bool useTokenBasedEncryption() const;
+
+ [[nodiscard]] const QString &getMnemonic() const;
+
+ void setCertificate(const QSslCertificate &certificate);
+
+ [[nodiscard]] const QSslCertificate& getCertificate() const;
+
+ [[nodiscard]] ENGINE* sslEngine() const;
+
+ [[nodiscard]] QByteArray generateSignatureCryptographicMessageSyntax(const QByteArray &data) const;
+
+ [[nodiscard]] bool verifySignatureCryptographicMessageSyntax(const QByteArray &cmsContent, const QByteArray &data, const QVector<QByteArray> &certificatePems) const;
+
+ [[nodiscard]] ClientSideEncryptionTokenSelector* usbTokenInformation();
+
+ [[nodiscard]] bool canEncrypt() const;
+
+ [[nodiscard]] bool canDecrypt() const;
+
+ [[nodiscard]] bool userCertificateNeedsMigration() const;
+
+ [[nodiscard]] QByteArray certificateSha256Fingerprint() const;
signals:
void initializationFinished(bool isNewMnemonicGenerated = false);
void certificateFetchedFromKeychain(QSslCertificate certificate);
void certificatesFetchedFromServer(const QHash<QString, OCC::NextcloudSslCertificate> &results);
void certificateWriteComplete(const QSslCertificate &certificate);
+ void displayTokenInitDialog();
-public:
- [[nodiscard]] QByteArray generateSignatureCryptographicMessageSyntax(const QByteArray &data) const;
- [[nodiscard]] bool verifySignatureCryptographicMessageSyntax(const QByteArray &cmsContent, const QByteArray &data, const QVector<QByteArray> &certificatePems) const;
+ void startingDiscoveryEncryptionUsbToken();
+ void finishedDiscoveryEncryptionUsbToken();
+
+ void canEncryptChanged();
+ void canDecryptChanged();
+ void userCertificateNeedsMigrationChanged();
public slots:
- void initialize(const OCC::AccountPtr &account);
+ void initialize(QWidget *settingsDialog,
+ const OCC::AccountPtr &account);
+ void initializeHardwareTokenEncryption(QWidget* settingsDialog,
+ const OCC::AccountPtr &account);
+ void addExtraRootCertificates();
void forgetSensitiveData(const OCC::AccountPtr &account);
void getUsersPublicKeyFromServer(const OCC::AccountPtr &account, const QStringList &userIds);
void fetchCertificateFromKeyChain(const OCC::AccountPtr &account, const QString &userId);
void writeCertificate(const OCC::AccountPtr &account, const QString &userId, const QSslCertificate &certificate);
+ void migrateCertificate();
+
private slots:
void generateKeyPair(const OCC::AccountPtr &account);
void encryptPrivateKey(const OCC::AccountPtr &account);
void writePrivateKey(const OCC::AccountPtr &account);
void writeCertificate(const OCC::AccountPtr &account);
+ void completeHardwareTokenInitialization(QWidget *settingsDialog,
+ const OCC::AccountPtr &account);
+
+ void setMnemonic(const QString &mnemonic);
+
private:
void generateMnemonic();
+ void setEncryptionCertificate(CertificateInformation certificateInfo);
+
[[nodiscard]] std::pair<QByteArray, PKey> generateCSR(const AccountPtr &account,
PKey keyPair,
PKey privateKey);
PKey keyPair,
const QByteArray &csrContent);
+ void sendPublicKey(const AccountPtr &account);
+
void writeKeyPair(const AccountPtr &account,
PKey keyPair,
const QByteArray &csrContent);
[[nodiscard]] bool checkServerPublicKeyValidity(const QByteArray &serverPublicKeyString) const;
[[nodiscard]] bool sensitiveDataRemaining() const;
+ [[nodiscard]] bool checkEncryptionIsWorking() const;
+
void failedToInitialize(const AccountPtr &account);
- bool isInitialized = false;
+ void saveCertificateIdentification(const AccountPtr &account) const;
+ void cacheTokenPin(const QString pin);
+
+ QString _mnemonic;
+ bool _newMnemonicGenerated = false;
+
+ QString _cachedPin;
+
+ ClientSideEncryptionTokenSelector _usbTokenInformation;
+
+ CertificateInformation _encryptionCertificate;
+ std::vector<CertificateInformation> _otherCertificates;
+
+ Pkcs11Context _context{Pkcs11Context::State::EmptyContext};
+ std::unique_ptr<PKCS11_SLOT[], std::function<void(PKCS11_SLOT*)>> _tokenSlots;
};
+
} // namespace OCC
+
#endif
#include "common/syncjournaldb.h"
Q_LOGGING_CATEGORY(lcSignPublicKeyApiJob, "nextcloud.sync.networkjob.sendcsr", QtInfoMsg)
+Q_LOGGING_CATEGORY(lcStorePublicKeyApiJob, "nextcloud.sync.networkjob.storepublickey", QtInfoMsg)
Q_LOGGING_CATEGORY(lcStorePrivateKeyApiJob, "nextcloud.sync.networkjob.storeprivatekey", QtInfoMsg)
Q_LOGGING_CATEGORY(lcCseJob, "nextcloud.sync.networkjob.clientsideencrypt", QtInfoMsg)
}
StoreMetaDataApiJob::StoreMetaDataApiJob(const AccountPtr& account,
- const QByteArray& fileId,
- const QByteArray &token,
- const QByteArray& b64Metadata,
- const QByteArray &signature,
- QObject* parent)
-: AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent),
-_fileId(fileId),
-_token(token),
-_b64Metadata(b64Metadata),
-_signature(signature)
+ const QByteArray& fileId,
+ const QByteArray &token,
+ const QByteArray& b64Metadata,
+ const QByteArray &signature,
+ QObject* parent)
+ : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent),
+ _fileId(fileId),
+ _token(token),
+ _b64Metadata(b64Metadata),
+ _signature(signature)
{
}
if (_account->capabilities().clientSideEncryptionVersion() >= 2.0) {
if (!_signature.isEmpty()) {
req.setRawHeader(e2eeSignatureHeaderName, _signature);
+ } else {
+ qCWarning(lcCseJob()) << "empty signature for" << _fileId;
}
}
QUrlQuery query;
bool StoreMetaDataApiJob::finished()
{
- int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
- if (retCode != 200) {
- qCInfo(lcCseJob()) << "error sending the metadata" << path() << errorString() << retCode;
- emit error(_fileId, retCode);
- return false;
- }
- qCInfo(lcCseJob()) << "Metadata submitted to the server successfully";
- emit success(_fileId);
+ const auto retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (retCode != 200) {
+ qCInfo(lcCseJob()) << "error sending the metadata" << path() << errorString() << retCode;
+ emit error(_fileId, retCode);
+ return false;
+ }
+
+ qCInfo(lcCseJob()) << "Metadata submitted to the server successfully";
+ emit success(_fileId);
+
return true;
}
LockEncryptFolderApiJob::LockEncryptFolderApiJob(const AccountPtr &account,
const QByteArray &fileId,
+ const QByteArray &certificateSha256Fingerprint,
SyncJournalDb *journalDb,
- const QSslKey publicKey,
+ const QSslKey &sslkey,
QObject *parent)
: AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("lock/") + fileId, parent)
, _fileId(fileId)
+ , _certificateSha256Fingerprint(certificateSha256Fingerprint)
, _journalDb(journalDb)
- , _publicKey(publicKey)
{
+ Q_UNUSED(sslkey)
}
void LockEncryptFolderApiJob::start()
if (!folderTokenEncrypted.isEmpty()) {
qCInfo(lcCseJob()) << "lock folder started for:" << path() << " for fileId: " << _fileId << " but we need to first lift the previous lock";
- const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->_privateKey, folderTokenEncrypted);
- const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, _fileId, folderToken, _journalDb, this);
+ const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), folderTokenEncrypted);
+ if (!folderToken) {
+ qCWarning(lcCseJob()) << "decrypt failed";
+ return;
+ }
+ const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, _fileId, *folderToken, _journalDb, this);
unlockJob->setShouldRollbackMetadataChanges(true);
connect(unlockJob, &UnlockEncryptFolderApiJob::done, this, [this]() {
this->start();
qCInfo(lcCseJob()) << "lock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId << " token:" << token;
- if (!_publicKey.isNull()) {
- const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_publicKey, token);
- _journalDb->setE2EeLockedFolder(_fileId, folderTokenEncrypted);
+ if (!_account->e2e()->getPublicKey().isNull()) {
+ const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_account->e2e()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), token);
+ if (!folderTokenEncrypted) {
+ qCWarning(lcCseJob()) << "decrypt failed";
+ return false;
+ }
+ _journalDb->setE2EeLockedFolder(_fileId, *folderTokenEncrypted);
}
//TODO: Parse the token and submit.
return true;
}
+StorePublicKeyApiJob::StorePublicKeyApiJob(const AccountPtr& account, const QString& path, QObject* parent)
+ : AbstractNetworkJob(account, path, parent)
+{
+}
+
+void StorePublicKeyApiJob::setPublicKey(const QByteArray& publicKey)
+{
+ QByteArray data = "publicKey=";
+ data += QUrl::toPercentEncoding(publicKey);
+ _publicKey.setData(data);
+}
+
+void StorePublicKeyApiJob::start()
+{
+ QNetworkRequest req;
+ req.setRawHeader("OCS-APIREQUEST", "true");
+ req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded"));
+ QUrlQuery query;
+ query.addQueryItem(QLatin1String("format"), QLatin1String("json"));
+ QUrl url = Utility::concatUrlPath(account()->url(), path());
+ url.setQuery(query);
+
+ qCDebug(lcStorePublicKeyApiJob) << "Sending the public key";
+ sendRequest("PUT", url, req, &_publicKey);
+ AbstractNetworkJob::start();
+}
+
+bool StorePublicKeyApiJob::finished()
+{
+ int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (retCode != 200)
+ qCInfo(lcStorePublicKeyApiJob()) << "Sending public key ended with" << path() << errorString() << retCode;
+
+ QJsonParseError error{};
+ auto json = QJsonDocument::fromJson(reply()->readAll(), &error);
+ emit jsonReceived(json, reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
+ return true;
+}
+
StorePrivateKeyApiJob::StorePrivateKeyApiJob(const AccountPtr& account, const QString& path, QObject* parent)
: AbstractNetworkJob(account, path, parent)
{
QUrl url = Utility::concatUrlPath(account()->url(), path());
url.setQuery(query);
- qCInfo(lcStorePrivateKeyApiJob) << "Sending the private key" << _privKey.data();
+ qCDebug(lcStorePrivateKeyApiJob) << "Sending the private key";
sendRequest("POST", url, req, &_privKey);
AbstractNetworkJob::start();
}
QUrl url = Utility::concatUrlPath(account()->url(), path());
url.setQuery(query);
- qCInfo(lcSignPublicKeyApiJob) << "Sending the CSR" << _csr.data();
+ qCDebug(lcSignPublicKeyApiJob) << "Sending the CSR";
sendRequest("POST", url, req, &_csr);
AbstractNetworkJob::start();
}
#include "networkjobs.h"
#include "accountfwd.h"
+
+#include <QSslKey>
#include <QString>
#include <QJsonDocument>
-#include <QSslKey>
namespace OCC {
/* Here are all of the network jobs for the client side encryption.
QBuffer _csr;
};
+/*
+ * @brief Job to upload the PublicKey that return JSON
+ *
+ * To be used like this:
+ * \code
+ * _job = new StorePublicKeyApiJob(account, QLatin1String("ocs/v1.php/foo/bar"), this);
+ * _job->setPublicKey( privKey );
+ * connect(_job...);
+ * _job->start();
+ * \encode
+ *
+ * @ingroup libsync
+ */
+class OWNCLOUDSYNC_EXPORT StorePublicKeyApiJob : public AbstractNetworkJob
+{
+ Q_OBJECT
+public:
+ explicit StorePublicKeyApiJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr);
+
+ /**
+ * @brief setCsr - the CSR with the public key.
+ * This function needs to be called before start() obviously.
+ */
+ void setPublicKey(const QByteArray& publicKey);
+
+public slots:
+ void start() override;
+
+protected:
+ bool finished() override;
+signals:
+
+ /**
+ * @brief jsonReceived - signal to report the json answer from ocs
+ * @param json - the parsed json document
+ * @param statusCode - the OCS status code: 100 (!) for success
+ */
+ void jsonReceived(const QJsonDocument &json, int statusCode);
+
+private:
+ QBuffer _publicKey;
+};
+
/*
* @brief Job to upload the PrivateKey that return JSON
*
{
Q_OBJECT
public:
- explicit LockEncryptFolderApiJob(const AccountPtr &account, const QByteArray &fileId, SyncJournalDb *journalDb, const QSslKey publicKey, QObject *parent = nullptr);
+ explicit LockEncryptFolderApiJob(const AccountPtr &account,
+ const QByteArray &fileId,
+ const QByteArray &certificateSha256Fingerprint,
+ SyncJournalDb *journalDb,
+ const QSslKey &sslkey,
+ QObject *parent = nullptr);
void setCounter(const quint64 counter);
private:
QByteArray _fileId;
+ QByteArray _certificateSha256Fingerprint;
QPointer<SyncJournalDb> _journalDb;
QSslKey _publicKey;
quint64 _counter = 0;
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
+
#include "clientsideencryptionprimitives.h"
+
+#include <QLoggingCategory>
+
#include <openssl/pem.h>
namespace OCC
{
-Bio::Bio()
- : _bio(BIO_new(BIO_s_mem()))
-{
-}
-Bio::~Bio()
-{
- BIO_free_all(_bio);
-}
+
+Q_LOGGING_CATEGORY(lcCseUtility, "nextcloud.sync.clientsideencryption.utility", QtInfoMsg)
+
Bio::operator const BIO *() const
{
return _bio;
{
PKeyCtx ctx;
ctx._ctx = EVP_PKEY_CTX_new(pkey, e);
+ Q_ASSERT(ctx._ctx);
return ctx;
}
return result;
}
+PKey PKey::readHardwarePublicKey(PKCS11_KEY *key)
+{
+ PKey result;
+ result._pkey = PKCS11_get_public_key(key);
+ return result;
+}
+
PKey PKey::readPrivateKey(Bio &bio)
{
PKey result;
return result;
}
+PKey PKey::readHardwarePrivateKey(PKCS11_KEY *key)
+{
+ PKey result;
+ result._pkey = PKCS11_get_private_key(key);
+ return result;
+}
+
PKey PKey::generate(PKeyCtx &ctx)
{
PKey result;
return _pkey;
}
-}
\ No newline at end of file
+Pkcs11Context::Pkcs11Context(State initState)
+ : _pkcsS11Ctx(initState == State::CreateContext ? PKCS11_CTX_new() : nullptr)
+{
+}
+
+Pkcs11Context::Pkcs11Context(Pkcs11Context &&otherContext)
+ : _pkcsS11Ctx(otherContext._pkcsS11Ctx)
+{
+ otherContext._pkcsS11Ctx = nullptr;
+}
+
+Pkcs11Context::~Pkcs11Context()
+{
+ if (_pkcsS11Ctx) {
+ PKCS11_CTX_free(_pkcsS11Ctx);
+ _pkcsS11Ctx = nullptr;
+ }
+}
+
+Pkcs11Context &Pkcs11Context::operator=(Pkcs11Context &&otherContext)
+{
+ if (&otherContext != this) {
+ if (_pkcsS11Ctx) {
+ PKCS11_CTX_free(_pkcsS11Ctx);
+ _pkcsS11Ctx = nullptr;
+ }
+ std::swap(_pkcsS11Ctx, otherContext._pkcsS11Ctx);
+ }
+
+ return *this;
+}
+
+void Pkcs11Context::clear()
+{
+ if (_pkcsS11Ctx) {
+ PKCS11_CTX_free(_pkcsS11Ctx);
+ _pkcsS11Ctx = nullptr;
+ }
+}
+
+}
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
+
#pragma once
-#include <openssl/evp.h>
+
#include <QtCore/qglobal.h>
+#include <openssl/evp.h>
+#include <libp11.h>
+
namespace OCC
{
class Bio
{
public:
- Bio();
+ Bio()
+ : _bio(BIO_new(BIO_s_mem()))
+ {
+ }
- ~Bio();
+ ~Bio()
+ {
+ BIO_free_all(_bio);
+ }
operator const BIO *() const;
operator BIO *();
static PKey readPublicKey(Bio &bio);
+ static PKey readHardwarePublicKey(PKCS11_KEY *key);
+
static PKey readPrivateKey(Bio &bio);
+ static PKey readHardwarePrivateKey(PKCS11_KEY *key);
+
static PKey generate(PKeyCtx &ctx);
operator EVP_PKEY *();
EVP_PKEY *_pkey = nullptr;
};
-}
\ No newline at end of file
+
+class Pkcs11Context {
+public:
+ enum class State {
+ CreateContext,
+ EmptyContext,
+ };
+
+ explicit Pkcs11Context(State initState);
+
+ Pkcs11Context(Pkcs11Context &&otherContext);
+
+ Pkcs11Context(const Pkcs11Context&) = delete;
+
+ ~Pkcs11Context();
+
+ Pkcs11Context& operator=(Pkcs11Context &&otherContext);
+
+ Pkcs11Context& operator=(const Pkcs11Context&) = delete;
+
+ operator const PKCS11_CTX*() const
+ {
+ return _pkcsS11Ctx;
+ }
+
+ operator PKCS11_CTX*()
+ {
+ return _pkcsS11Ctx;
+ }
+
+ void clear();
+
+private:
+ PKCS11_CTX* _pkcsS11Ctx = nullptr;
+};
+
+}
--- /dev/null
+/*
+ * Copyright © 2023, Matthieu Gallien <matthieu.gallien@nextcloud.com>
+ * Copyright (C) 2017 The Qt Company Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * Commercial License Usage
+ * Licensees holding valid commercial Qt licenses may use this file in
+ * accordance with the commercial license agreement provided with the
+ * Software or, alternatively, in accordance with the terms contained in
+ * a written agreement between you and The Qt Company. For licensing terms
+ * and conditions see https://www.qt.io/terms-conditions. For further
+ * information use the contact form at https://www.qt.io/contact-us.
+ *
+ * GNU Lesser General Public License Usage
+ * Alternatively, this file may be used under the terms of the GNU Lesser
+ * General Public License version 3 as published by the Free Software
+ * Foundation and appearing in the file LICENSE.LGPL3 included in the
+ * packaging of this file. Please review the following information to
+ * ensure the GNU Lesser General Public License version 3 requirements
+ * will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+ *
+ * GNU General Public License Usage
+ * Alternatively, this file may be used under the terms of the GNU
+ * General Public License version 2.0 or (at your option) the GNU General
+ * Public license version 3 or any later version approved by the KDE Free
+ * Qt Foundation. The licenses are as published by the Free Software
+ * Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+ * included in the packaging of this file. Please review the following
+ * information to ensure the GNU General Public License requirements will
+ * be met: https://www.gnu.org/licenses/gpl-2.0.html and
+ * https://www.gnu.org/licenses/gpl-3.0.html.
+ */
+
+#include "clientsideencryptiontokenselector.h"
+
+#include "clientsideencryptionprimitives.h"
+#include "account.h"
+
+#include <QLoggingCategory>
+#include <QtConcurrentRun>
+#include <QSslCertificateExtension>
+
+#if defined(Q_OS_WIN)
+#include <wincrypt.h>
+#endif
+
+#include <libp11.h>
+
+#include <openssl/pem.h>
+
+namespace {
+
+static unsigned char* unsignedData(QByteArray& array)
+{
+ return (unsigned char*)array.data();
+}
+
+static QByteArray BIO2ByteArray(OCC::Bio &b) {
+ auto pending = static_cast<int>(BIO_ctrl_pending(b));
+ QByteArray res(pending, '\0');
+ BIO_read(b, unsignedData(res), pending);
+ return res;
+}
+
+}
+
+namespace OCC
+{
+
+Q_LOGGING_CATEGORY(lcCseSelector, "nextcloud.sync.clientsideencryption.selector", QtInfoMsg)
+
+ClientSideEncryptionTokenSelector::ClientSideEncryptionTokenSelector(QObject *parent)
+ : QObject{parent}
+{
+
+}
+
+bool ClientSideEncryptionTokenSelector::isSetup() const
+{
+ return !_sha256Fingerprint.isEmpty();
+}
+
+QVariantList ClientSideEncryptionTokenSelector::discoveredCertificates() const
+{
+ return _discoveredCertificates;
+}
+
+QByteArray ClientSideEncryptionTokenSelector::sha256Fingerprint() const
+{
+ return _sha256Fingerprint;
+}
+
+void ClientSideEncryptionTokenSelector::clear()
+{
+ _discoveredCertificates.clear();
+ _sha256Fingerprint.clear();
+}
+
+QFuture<void> ClientSideEncryptionTokenSelector::searchForCertificates(const AccountPtr &account)
+{
+ return QtConcurrent::run([this, account] () -> void {
+ discoverCertificates(account);
+ });
+}
+
+void ClientSideEncryptionTokenSelector::setSha256Fingerprint(const QByteArray &sha256Fingerprint)
+{
+ if (_sha256Fingerprint == sha256Fingerprint) {
+ return;
+ }
+
+ _sha256Fingerprint = sha256Fingerprint;
+ Q_EMIT sha256FingerprintChanged();
+}
+
+void ClientSideEncryptionTokenSelector::discoverCertificates(const AccountPtr &account)
+{
+ auto ctx = Pkcs11Context{Pkcs11Context::State::CreateContext};
+
+ auto rc = PKCS11_CTX_load(ctx, account->encryptionHardwareTokenDriverPath().toLatin1().constData());
+ if (rc) {
+ qCWarning(lcCseSelector()) << "loading pkcs11 engine failed:" << ERR_reason_error_string(ERR_get_error()) << account->encryptionHardwareTokenDriverPath();
+
+ Q_EMIT failedToInitialize(account);
+ return;
+ }
+
+ auto tokensCount = 0u;
+ PKCS11_SLOT *tempTokenSlots = nullptr;
+ /* get information on all slots */
+ if (PKCS11_enumerate_slots(ctx, &tempTokenSlots, &tokensCount) < 0) {
+ qCWarning(lcCseSelector()) << "no slots available" << ERR_reason_error_string(ERR_get_error());
+
+ Q_EMIT failedToInitialize(account);
+ return;
+ }
+
+ auto deleter = [&ctx, tokensCount] (PKCS11_SLOT* pointer) noexcept -> void {
+ PKCS11_release_all_slots(ctx, pointer, tokensCount);
+ };
+
+ auto tokenSlots = std::unique_ptr<PKCS11_SLOT[], decltype(deleter)>{tempTokenSlots, deleter};
+
+ if (!tokensCount) {
+ qCWarning(lcCseSelector()) << "no tokens found";
+
+ Q_EMIT failedToInitialize(account);
+ return;
+ }
+
+ _discoveredCertificates.clear();
+ auto currentSlot = static_cast<PKCS11_SLOT*>(nullptr);
+ for(auto tokenIndex = 0u; tokenIndex < tokensCount; ++tokenIndex) {
+ currentSlot = PKCS11_find_next_token(ctx, tokenSlots.get(), tokensCount, currentSlot);
+ if (currentSlot == nullptr || currentSlot->token == nullptr) {
+ break;
+ }
+
+ qCDebug(lcCseSelector()) << "Slot manufacturer......:" << currentSlot->manufacturer;
+ qCDebug(lcCseSelector()) << "Slot description.......:" << currentSlot->description;
+ qCDebug(lcCseSelector()) << "Slot token label.......:" << currentSlot->token->label;
+ qCDebug(lcCseSelector()) << "Slot token manufacturer:" << currentSlot->token->manufacturer;
+ qCDebug(lcCseSelector()) << "Slot token model.......:" << currentSlot->token->model;
+ qCDebug(lcCseSelector()) << "Slot token serialnr....:" << currentSlot->token->serialnr;
+
+ auto keysCount = 0u;
+ auto certificatesFromToken = static_cast<PKCS11_CERT*>(nullptr);
+ if (PKCS11_enumerate_certs(currentSlot->token, &certificatesFromToken, &keysCount)) {
+ qCWarning(lcCseSelector()) << "PKCS11_enumerate_certs failed" << ERR_reason_error_string(ERR_get_error());
+
+ Q_EMIT failedToInitialize(account);
+ return;
+ }
+
+ for (auto certificateIndex = 0u; certificateIndex < keysCount; ++certificateIndex) {
+ const auto currentCertificate = &certificatesFromToken[certificateIndex];
+ qCInfo(lcCseSelector()) << "certificate metadata:"
+ << "label:" << currentCertificate->label;
+
+ const auto certificateId = QByteArray{reinterpret_cast<char*>(currentCertificate->id), static_cast<int>(currentCertificate->id_len)};
+ qCInfo(lcCseSelector()) << "new certificate ID:" << certificateId.toBase64();
+
+ Bio out;
+ const auto ret = PEM_write_bio_X509(out, currentCertificate->x509);
+ if (ret <= 0){
+ qCWarning(lcCseSelector()) << "PEM_write_bio_X509 failed" << ERR_reason_error_string(ERR_get_error());
+
+ Q_EMIT failedToInitialize(account);
+ return;
+ }
+
+ const auto result = BIO2ByteArray(out);
+ const auto sslCertificate = QSslCertificate{result, QSsl::Pem};
+ const auto certificateDigest = sslCertificate.digest(QCryptographicHash::Sha256).toBase64();
+
+ qCInfo(lcCseSelector()) << "newly found certificate"
+ << "subject:" << sslCertificate.subjectDisplayName()
+ << "issuer:" << sslCertificate.issuerDisplayName()
+ << "valid since:" << sslCertificate.effectiveDate()
+ << "valid until:" << sslCertificate.expiryDate()
+ << "serial number:" << sslCertificate.serialNumber()
+ << "SHA256 fingerprint:" << certificateDigest;
+
+ if (sslCertificate.isSelfSigned()) {
+ qCDebug(lcCseSelector()) << "newly found certificate is self signed: goint to ignore it";
+ continue;
+ }
+
+ auto hasNeededExtendedKeyUsageExtension = false;
+ const auto &allExtensions = sslCertificate.extensions();
+ for (const auto &oneExtension : allExtensions) {
+ qCDebug(lcCseSelector()) << "extension:" << (oneExtension.isCritical() ? "is critical" : "") << (oneExtension.isSupported() ? "is supported" : "") << oneExtension.name() << oneExtension.value() << oneExtension.oid();
+ if (oneExtension.oid() == QStringLiteral("2.5.29.37")) {
+ const auto extendedKeyUsageList = oneExtension.value().toList();
+ for (const auto &oneExtendedKeyUsageValue : extendedKeyUsageList) {
+ qCDebug(lcCseSelector()) << "EKU:" << oneExtendedKeyUsageValue;
+ if (oneExtendedKeyUsageValue == QStringLiteral("E-mail Protection")) {
+ hasNeededExtendedKeyUsageExtension = true;
+ break;
+ }
+ }
+ }
+ }
+ if (!hasNeededExtendedKeyUsageExtension) {
+ qCDebug(lcCseSelector()) << "newly found certificate is missing the required EKU extension: Secure Email (1.3.6.1.5.5.7.3.4)";
+ continue;
+ }
+
+ _discoveredCertificates.push_back(QVariantMap{
+ {QStringLiteral("label"), QString::fromLatin1(currentCertificate->label)},
+ {QStringLiteral("subject"), sslCertificate.subjectDisplayName()},
+ {QStringLiteral("issuer"), sslCertificate.issuerDisplayName()},
+ {QStringLiteral("serialNumber"), sslCertificate.serialNumber()},
+ {QStringLiteral("validSince"), sslCertificate.effectiveDate()},
+ {QStringLiteral("validUntil"), sslCertificate.expiryDate()},
+ {QStringLiteral("sha256Fingerprint"), certificateDigest},
+ {QStringLiteral("certificate"), QVariant::fromValue(sslCertificate)},
+ });
+
+ std::sort(_discoveredCertificates.begin(), _discoveredCertificates.end(), [] (const auto &first, const auto &second) -> bool {
+ return first.toMap()[QStringLiteral("validSince")].toDateTime() > second.toMap()[QStringLiteral("validSince")].toDateTime();
+ });
+ }
+ }
+
+ processDiscoveredCertificates();
+}
+
+void ClientSideEncryptionTokenSelector::processDiscoveredCertificates()
+{
+ const auto &allCertificates = discoveredCertificates();
+ for (const auto &oneCertificate : allCertificates) {
+ const auto certificateData = oneCertificate.toMap();
+ const auto sslCertificate = certificateData[QStringLiteral("certificate")].value<QSslCertificate>();
+ if (sslCertificate.isNull()) {
+ qCDebug(lcCseSelector()) << "null certificate";
+ continue;
+ }
+ const auto sslErrors = QSslCertificate::verify({sslCertificate});
+ if (!sslErrors.isEmpty()) {
+ for (const auto &oneError : sslErrors) {
+ qCInfo(lcCseSelector()) << oneError;
+ }
+ continue;
+ }
+
+ const auto &sha256Fingerprint = sslCertificate.digest(QCryptographicHash::Sha256).toBase64();
+ qCInfo(lcCseSelector()) << "selected certificate" << certificateData[QStringLiteral("subject")] << "from" << certificateData[QStringLiteral("issuer")] << "fingerprint" << sha256Fingerprint << "serialNumber" << sslCertificate.serialNumber();
+
+ setSha256Fingerprint(sha256Fingerprint);
+ Q_EMIT isSetupChanged();
+ return;
+ }
+}
+
+}
--- /dev/null
+/*
+ * Copyright © 2023, Matthieu Gallien <matthieu.gallien@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#ifndef CLIENTSIDETOKENSELECTOR_H
+#define CLIENTSIDETOKENSELECTOR_H
+
+#include "accountfwd.h"
+#include "owncloudlib.h"
+
+#include <QObject>
+#include <QFuture>
+
+namespace OCC
+{
+
+class OWNCLOUDSYNC_EXPORT ClientSideEncryptionTokenSelector : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(bool isSetup READ isSetup NOTIFY isSetupChanged)
+
+ Q_PROPERTY(QByteArray sha256Fingerprint READ sha256Fingerprint WRITE setSha256Fingerprint NOTIFY sha256FingerprintChanged)
+
+public:
+ explicit ClientSideEncryptionTokenSelector(QObject *parent = nullptr);
+
+ [[nodiscard]] bool isSetup() const;
+
+ [[nodiscard]] QByteArray sha256Fingerprint() const;
+
+ void clear();
+
+public slots:
+ QFuture<void> searchForCertificates(const OCC::AccountPtr &account);
+
+ void setSha256Fingerprint(const QByteArray &sha256Fingerprint);
+
+signals:
+
+ void isSetupChanged();
+
+ void sha256FingerprintChanged();
+
+ void failedToInitialize(const OCC::AccountPtr &account);
+
+private:
+ void discoverCertificates(const OCC::AccountPtr &account);
+
+ [[nodiscard]] QVariantList discoveredCertificates() const;
+
+ void processDiscoveredCertificates();
+
+ QVariantList _discoveredCertificates;
+
+ QByteArray _sha256Fingerprint;
+};
+
+}
+
+#endif // CLIENTSIDETOKENSELECTOR_H
{
qCInfo(lcDisco) << "STARTING" << _currentFolder._server << _queryServer << _currentFolder._local << _queryLocal;
+ if (isInsideEncryptedTree()) {
+ auto folderDbRecord = SyncJournalFileRecord{};
+ if (_discoveryData->_statedb->getFileRecord(_currentFolder._local, &folderDbRecord) && folderDbRecord.isValid()) {
+ if (_discoveryData->_account->encryptionCertificateFingerprint() != folderDbRecord._e2eCertificateFingerprint) {
+ qCDebug(lcDisco) << "encryption certificate needs update. Forcing full discovery";
+ _queryServer = NormalQuery;
+ }
+ }
+ }
+
_discoveryData->_noCaseConflictRecordsInDb = _discoveryData->_statedb->caseClashConflictRecordPaths().isEmpty();
if (_queryServer == NormalQuery) {
continue;
const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted() &&
- _discoveryData->_account->e2e() && !_discoveryData->_account->e2e()->_publicKey.isNull() && _discoveryData->_account->e2e()->_privateKey.isNull();
+ _discoveryData->_account->e2e() && !_discoveryData->_account->e2e()->isInitialized();
if (isEncryptedFolderButE2eIsNotSetup) {
checkAndUpdateSelectiveSyncListsForE2eeFolders(path._server + "/");
item->_e2eEncryptionStatus = serverEntry.isE2eEncrypted() ? SyncFileItem::EncryptionStatus::Encrypted : SyncFileItem::EncryptionStatus::NotEncrypted;
if (serverEntry.isE2eEncrypted()) {
item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_discoveryData->_account->capabilities().clientSideEncryptionVersion());
+ item->_e2eCertificateFingerprint = serverEntry.e2eCertificateFingerprint;
}
item->_encryptedFileName = [=] {
if (serverEntry.e2eMangledName.isEmpty()) {
if (dbEntry.isValid()) {
bool typeChange = localEntry.isDirectory != dbEntry.isDirectory();
- if (!typeChange && localEntry.isVirtualFile) {
+ if (localEntry.isDirectory && dbEntry.isValid() && dbEntry.isE2eEncrypted() && dbEntry._e2eCertificateFingerprint != _discoveryData->_account->encryptionCertificateFingerprint()) {
+ item->_instruction = CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA;
+ item->_direction = SyncFileItem::Up;
+ } else if (!typeChange && localEntry.isVirtualFile) {
if (noServerEntry) {
item->_instruction = CSYNC_INSTRUCTION_REMOVE;
item->_direction = SyncFileItem::Down;
// base is a record in the SyncJournal database that contains the data about the being-renamed folder with it's old name and encryption information
item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(base._e2eEncryptionStatus);
item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_discoveryData->_account->capabilities().clientSideEncryptionVersion());
+ item->_e2eCertificateFingerprint = base._e2eCertificateFingerprint;
}
postProcessLocalNew();
finalize();
}
}
+ if (item->_direction == SyncFileItem::Up && item->isEncrypted() && !_discoveryData->_account->e2e()->canEncrypt()) {
+ item->_instruction = CSYNC_INSTRUCTION_ERROR;
+ item->_errorString = tr("Cannot modify encrypted item because the selected certificate is not valid.");
+ item->_status = SyncFileItem::Status::NormalError;
+ }
+
if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_SYNC)
item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
bool removed = item->_instruction == CSYNC_INSTRUCTION_REMOVE;
const auto alreadyDownloaded = _discoveryData->_statedb->getFileRecord(_dirItem->_file, &record) && record.isValid();
// we need to make sure we first download all e2ee files/folders before migrating
_dirItem->_isEncryptedMetadataNeedUpdate = alreadyDownloaded && serverJob->encryptedMetadataNeedUpdate();
+ _dirItem->_e2eCertificateFingerprint = serverJob->certificateSha256Fingerprint();
_dirItem->_e2eEncryptionStatus = serverJob->currentEncryptionStatus();
_dirItem->_e2eEncryptionStatusRemote = serverJob->currentEncryptionStatus();
_dirItem->_e2eEncryptionServerCapability = serverJob->requiredEncryptionStatus();
return _encryptionStatusRequired;
}
+QByteArray DiscoverySingleDirectoryJob::certificateSha256Fingerprint() const
+{
+ return _e2eCertificateFingerprint;
+}
+
static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemotePermissions::MountedPermissionAlgorithm algorithm, RemoteInfo &result)
{
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
}
_isFileDropDetected = e2EeFolderMetadata->isFileDropPresent();
_encryptedMetadataNeedUpdate = e2EeFolderMetadata->encryptedMetadataNeedUpdate();
+ _e2eCertificateFingerprint = e2EeFolderMetadata->certificateSha256Fingerprint();
_encryptionStatusRequired = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_account->capabilities().clientSideEncryptionVersion());
_encryptionStatusCurrent = e2EeFolderMetadata->existingMetadataEncryptionStatus();
bool _isE2eEncrypted = false;
bool isFileDropDetected = false;
QString e2eMangledName;
+ QByteArray e2eCertificateFingerprint;
bool sharedByMe = false;
[[nodiscard]] bool isValid() const { return !name.isNull(); }
void abort();
[[nodiscard]] bool isFileDropDetected() const;
[[nodiscard]] bool encryptedMetadataNeedUpdate() const;
+ [[nodiscard]] QByteArray certificateSha256Fingerprint() const;
[[nodiscard]] SyncFileItem::EncryptionStatus currentEncryptionStatus() const;
[[nodiscard]] SyncFileItem::EncryptionStatus requiredEncryptionStatus() const;
bool _isFileDropDetected = false;
bool _encryptedMetadataNeedUpdate = false;
SyncFileItem::EncryptionStatus _encryptionStatusRequired = SyncFileItem::EncryptionStatus::NotEncrypted;
+ QByteArray _e2eCertificateFingerprint;
+
// If set, the discovery will finish with an error
int64_t _size = 0;
QString _error;
return;
}
- const auto lockJob = new LockEncryptFolderApiJob(_account, _folderId, _journalDb, _account->e2e()->_publicKey, this);
+ const auto lockJob = new LockEncryptFolderApiJob(_account, _folderId, _account->e2e()->certificateSha256Fingerprint(), _journalDb, _account->e2e()->getPublicKey(), this);
connect(lockJob, &LockEncryptFolderApiJob::success, this, &EncryptedFolderMetadataHandler::slotFolderLockedSuccessfully);
connect(lockJob, &LockEncryptFolderApiJob::error, this, &EncryptedFolderMetadataHandler::slotFolderLockedError);
if (_account->capabilities().clientSideEncryptionVersion() >= 2.0) {
qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Calling Unlock";
const auto unlockJob = new UnlockEncryptFolderApiJob(_account, _folderId, _folderToken, _journalDb, this);
- connect(unlockJob, &UnlockEncryptFolderApiJob::success, [this](const QByteArray &folderId) {
+ connect(unlockJob, &UnlockEncryptFolderApiJob::success, unlockJob, [this](const QByteArray &folderId) {
qDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Successfully Unlocked";
_isFolderLocked = false;
emit folderUnlocked(folderId, 200);
_isUnlockRunning = false;
});
- connect(unlockJob, &UnlockEncryptFolderApiJob::error, [this](const QByteArray &folderId, int httpStatus) {
+ connect(unlockJob, &UnlockEncryptFolderApiJob::error, unlockJob, [this](const QByteArray &folderId, int httpStatus) {
qDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Unlock Error";
emit folderUnlocked(folderId, httpStatus);
_isUnlockRunning = false;
if (!rec.isE2eEncrypted()) {
rec._e2eEncryptionStatus = SyncJournalFileRecord::EncryptionStatus::Encrypted;
+ rec._e2eCertificateFingerprint = _account->e2e()->certificateSha256Fingerprint();
const auto result = _journal->setFileRecord(rec);
if (!result) {
qCWarning(lcEncryptFolderJob) << "Error when setting the file record to the database" << rec._path << result.error();
namespace OCC
{
-Q_LOGGING_CATEGORY(lcCseMetadata, "nextcloud.metadata", QtInfoMsg)
+Q_LOGGING_CATEGORY(lcCseMetadata, "nextcloud.sync.clientsideencryption.metadata", QtInfoMsg)
namespace
{
return;
}
- if (!parseFileDropPart(metaDataDoc)) {
- qCDebug(lcCseMetadata()) << "Could not parse filedrop part";
- return;
- }
-
if (_folderUsers.contains(_account->davUser())) {
const auto currentFolderUser = _folderUsers.value(_account->davUser());
- _metadataKeyForEncryption = decryptDataWithPrivateKey(currentFolderUser.encryptedMetadataKey);
+ _e2eCertificateFingerprint = QSslCertificate{currentFolderUser.certificatePem}.digest(QCryptographicHash::Sha256).toBase64();
+ _metadataKeyForEncryption = QByteArray::fromBase64(decryptDataWithPrivateKey(currentFolderUser.encryptedMetadataKey, _e2eCertificateFingerprint));
_metadataKeyForDecryption = _metadataKeyForEncryption;
}
+ if (!parseFileDropPart(metaDataDoc)) {
+ qCDebug(lcCseMetadata()) << "Could not parse filedrop part";
+ return;
+ }
+
if (metadataKeyForDecryption().isEmpty() || metadataKeyForEncryption().isEmpty()) {
qCDebug(lcCseMetadata()) << "Could not setup metadata key!";
_account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return;
}
-
+
const auto metadataObj = metaDataDoc.object()[metadataJsonKey].toObject();
_metadataNonce = QByteArray::fromBase64(metadataObj[nonceKey].toString().toLocal8Bit());
const auto cipherTextEncrypted = metadataObj[cipherTextKey].toString().toLocal8Bit();
const auto metadataKeyFromJson = metadataObj[metadataKeyKey].toString().toLocal8Bit();
if (!metadataKeyFromJson.isEmpty()) {
// parse version 1.1 and 1.2 (both must have a single "metadataKey"), not "metadataKeys" as 1.0
- const auto decryptedMetadataKeyBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(metadataKeyFromJson));
+ const auto decryptedMetadataKeyBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(metadataKeyFromJson), _account->e2e()->certificateSha256Fingerprint());
if (!decryptedMetadataKeyBase64.isEmpty()) {
// fromBase64() multiple times just to stick with the old wrong way
_metadataKeyForDecryption = QByteArray::fromBase64(QByteArray::fromBase64(decryptedMetadataKeyBase64));
if (!lastMetadataKeyFromJson.isEmpty()) {
const auto lastMetadataKeyValueFromJson = metadataKeys.value(lastMetadataKeyFromJson).toString().toLocal8Bit();
if (!lastMetadataKeyValueFromJson.isEmpty()) {
- const auto lastMetadataKeyValueFromJsonBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(lastMetadataKeyValueFromJson));
+ const auto lastMetadataKeyValueFromJsonBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(lastMetadataKeyValueFromJson), _account->e2e()->certificateSha256Fingerprint());
if (!lastMetadataKeyValueFromJsonBase64.isEmpty()) {
_metadataKeyForDecryption = QByteArray::fromBase64(QByteArray::fromBase64(lastMetadataKeyValueFromJsonBase64));
}
}
// RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key.
-QByteArray FolderMetadata::encryptDataWithPublicKey(const QByteArray &data, const QSslKey &key) const
+QByteArray FolderMetadata::encryptDataWithPublicKey(const QByteArray &binaryData,
+ const CertificateInformation &shareUserCertificate) const
{
- Bio publicKeyBio;
- const auto publicKeyPem = key.toPem();
- BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size());
- const auto publicKey = PKey::readPublicKey(publicKeyBio);
+ const auto encryptBase64Result = EncryptionHelper::encryptStringAsymmetric(shareUserCertificate, _account->e2e()->paddingMode(), *_account->e2e(), binaryData);
- // The metadata key is binary so base64 encode it first
- return EncryptionHelper::encryptStringAsymmetric(publicKey, data);
+ if (encryptBase64Result) {
+ return *encryptBase64Result;
+ } else {
+ qCWarning(lcCseMetadata()) << "fail to encryptDataWithPublicKey";
+ _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
+ return {};
+ }
+ return {};
}
-QByteArray FolderMetadata::decryptDataWithPrivateKey(const QByteArray &data) const
+QByteArray FolderMetadata::decryptDataWithPrivateKey(const QByteArray &base64Data,
+ const QByteArray &certificateFingerprint) const
{
- Bio privateKeyBio;
- BIO_write(privateKeyBio, _account->e2e()->_privateKey.constData(), _account->e2e()->_privateKey.size());
- const auto privateKey = PKey::readPrivateKey(privateKeyBio);
-
- const auto decryptResult = EncryptionHelper::decryptStringAsymmetric(privateKey, data);
- if (decryptResult.isEmpty()) {
+ const auto decryptBase64Result = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->getCertificateInformationByFingerprint(certificateFingerprint), _account->e2e()->paddingMode(), *_account->e2e(), base64Data);
+ if (!decryptBase64Result) {
qCDebug(lcCseMetadata()) << "ERROR. Could not decrypt the metadata key";
_account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
+ return {};
}
- return decryptResult;
+ return *decryptBase64Result;
}
// AES/GCM/NoPadding (128 bit key size)
{
auto hashAlgorithm = QCryptographicHash{QCryptographicHash::Sha256};
- hashAlgorithm.addData(_account->e2e()->_mnemonic.remove(' ').toUtf8());
+ auto mnemonic = _account->e2e()->getMnemonic();
+ hashAlgorithm.addData(mnemonic.remove(' ').toUtf8());
auto sortedFiles = _files;
std::sort(sortedFiles.begin(), sortedFiles.end(), [](const auto &first, const auto &second) {
return first.encryptedFilename < second.encryptedFilename;
}
qCDebug(lcCseMetadata()) << "Setting up empty metadata v2";
if (_isRootEncryptedFolder) {
- if (!addUser(_account->davUser(), _account->e2e()->_certificate)) {
+ if (!addUser(_account->davUser(), _account->e2e()->getCertificate())) {
qCDebug(lcCseMetadata) << "Empty metadata setup failed. Could not add first user.";
_account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return;
qCDebug(lcCseMetadata) << "Settint up legacy empty metadata";
_metadataKeyForEncryption = EncryptionHelper::generateRandom(metadataKeySize);
_metadataKeyForDecryption = _metadataKeyForEncryption;
- QString publicKey = _account->e2e()->_publicKey.toPem().toBase64();
+ QString publicKey = _account->e2e()->getPublicKey().toPem().toBase64();
QString displayName = _account->displayName();
_isMetadataValid = true;
}
const auto version = _account->capabilities().clientSideEncryptionVersion();
// multiple toBase64() just to keep with the old (wrong way)
- const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption().toBase64().toBase64(), _account->e2e()->_publicKey).toBase64();
+ const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption().toBase64().toBase64(), _account->e2e()->getCertificateInformation()).toBase64();
const QJsonObject metadata{
{versionKey, version},
{metadataKeyKey, QJsonValue::fromVariant(encryptedMetadataKey)},
if (userParsedId == _account->davUser()) {
const auto fileDropEntryUser = UserWithFileDropEntryAccess{
userParsedId,
- decryptDataWithPrivateKey(QByteArray::fromBase64(userParsed.value(usersEncryptedFiledropKey).toByteArray()))};
+ decryptDataWithPrivateKey(QByteArray::fromBase64(userParsed.value(usersEncryptedFiledropKey).toByteArray()), _e2eCertificateFingerprint)};
if (!fileDropEntryUser.isValid()) {
qCDebug(lcCseMetadata()) << "Could not parse filedrop data. encryptedFiledropKey decryption failed";
_account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return !foundNestedFoldersOrIsNestedFolder;
}
+QByteArray FolderMetadata::certificateSha256Fingerprint() const
+{
+ return _e2eCertificateFingerprint;
+}
+
bool FolderMetadata::moveFromFileDropToFiles()
{
if (_fileDropEntries.isEmpty()) {
bool FolderMetadata::addUser(const QString &userId, const QSslCertificate &certificate)
{
Q_ASSERT(_isRootEncryptedFolder);
+ Q_ASSERT(!certificate.isNull());
if (!_isRootEncryptedFolder) {
qCWarning(lcCseMetadata()) << "Could not add a folder user to a non top level folder.";
return false;
}
- const auto certificatePublicKey = certificate.publicKey();
- if (userId.isEmpty() || certificate.isNull() || certificatePublicKey.isNull()) {
+ const auto shareUserCertificate = CertificateInformation{{}, QSslCertificate{certificate}};
+ if (userId.isEmpty() || certificate.isNull() || !shareUserCertificate.canEncrypt()) {
qCWarning(lcCseMetadata()) << "Could not add a folder user. Invalid userId or certificate.";
return false;
}
UserWithFolderAccess newFolderUser;
newFolderUser.userId = userId;
newFolderUser.certificatePem = certificate.toPem();
- newFolderUser.encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), certificatePublicKey);
+ newFolderUser.encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), shareUserCertificate);
_folderUsers[userId] = newFolderUser;
updateUsersEncryptedMetadataKey();
auto folderUser = it.value();
const QSslCertificate certificate(folderUser.certificatePem);
- const auto certificatePublicKey = certificate.publicKey();
- if (certificate.isNull() || certificatePublicKey.isNull()) {
- qCWarning(lcCseMetadata()) << "Could not update folder users with null certificatePublicKey!";
- continue;
- }
+ CertificateInformation shareUserCertificate = CertificateInformation{{}, QSslCertificate{certificate}};
- const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), certificatePublicKey);
+ const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), shareUserCertificate);
if (encryptedMetadataKey.isEmpty()) {
qCWarning(lcCseMetadata()) << "Could not update folder users with empty encryptedMetadataKey!";
continue;
[[nodiscard]] bool encryptedMetadataNeedUpdate() const;
+ [[nodiscard]] QByteArray certificateSha256Fingerprint() const;
+
[[nodiscard]] bool moveFromFileDropToFiles();
// adds a user to have access to this folder (always generates new metadata key)
[[nodiscard]] QByteArray initialMetadata() const;
public slots:
- void addEncryptedFile(const EncryptedFile &f);
- void removeEncryptedFile(const EncryptedFile &f);
+ void addEncryptedFile(const FolderMetadata::EncryptedFile &f);
+ void removeEncryptedFile(const FolderMetadata::EncryptedFile &f);
void removeAllEncryptedFiles();
private:
[[nodiscard]] bool verifyMetadataKey(const QByteArray &metadataKey) const;
- [[nodiscard]] QByteArray encryptDataWithPublicKey(const QByteArray &data, const QSslKey &key) const;
- [[nodiscard]] QByteArray decryptDataWithPrivateKey(const QByteArray &data) const;
+ [[nodiscard]] QByteArray encryptDataWithPublicKey(const QByteArray &data,
+ const CertificateInformation &shareUserCertificate) const;
+ [[nodiscard]] QByteArray decryptDataWithPrivateKey(const QByteArray &data, const QByteArray &certificateFingerprint) const;
[[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const;
[[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const;
// signature from server-side metadata
QByteArray _initialSignature;
+ QByteArray _e2eCertificateFingerprint;
+
// both files and folders info
QVector<EncryptedFile> _files;
}
case CSYNC_INSTRUCTION_UPDATE_VFS_METADATA:
return new PropagateVfsUpdateMetadataJob(this, item);
+ case CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA:
+ {
+ const auto rootE2eeFolderPath = item->_file.split('/').first();
+ const auto rootE2eeFolderPathFullRemotePath = fullRemotePath(rootE2eeFolderPath);
+ return new UpdateMigratedE2eeMetadataJob(this, item, rootE2eeFolderPathFullRemotePath, remotePath());
+ }
case CSYNC_INSTRUCTION_IGNORE:
case CSYNC_INSTRUCTION_ERROR:
return new PropagateIgnoreJob(this, item);
- default:
+ case CSYNC_INSTRUCTION_NONE:
+ case CSYNC_INSTRUCTION_EVAL:
+ case CSYNC_INSTRUCTION_EVAL_RENAME:
+ case CSYNC_INSTRUCTION_STAT_ERROR:
+ case CSYNC_INSTRUCTION_UPDATE_METADATA:
+ case CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT:
return nullptr;
}
return nullptr;
}
#endif
if (!_item->_isAnyCaseClashChild && !_item->_isAnyInvalidCharChild) {
+ if (_item->isEncrypted()) {
+ _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint();
+ }
const auto result = propagator()->updateMetadata(*_item);
if (!result) {
status = _item->_status = SyncFileItem::FatalError;
return QCoreApplication::translate("progress", "Updated local metadata");
case CSYNC_INSTRUCTION_UPDATE_VFS_METADATA:
return QCoreApplication::translate("progress", "Updated local virtual files metadata");
+ case CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA:
+ return QCoreApplication::translate("progress", "Updated end-to-end encryption metadata");
case CSYNC_INSTRUCTION_NONE:
case CSYNC_INSTRUCTION_EVAL:
return QCoreApplication::translate("progress", "Unknown");
return QCoreApplication::translate("progress", "Updating local metadata");
case CSYNC_INSTRUCTION_UPDATE_VFS_METADATA:
return QCoreApplication::translate("progress", "Updating local virtual files metadata");
+ case CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA:
+ return QCoreApplication::translate("progress", "Updating end-to-end encryption metadata");
case CSYNC_INSTRUCTION_NONE:
case CSYNC_INSTRUCTION_EVAL:
break;
{
if (isEncrypted()) {
if (_downloadEncryptedHelper->decryptFile(_tmpFile)) {
+ _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint();
downloadFinished();
} else {
done(SyncFileItem::NormalError, _downloadEncryptedHelper->errorString(), ErrorCategory::GenericError);
const auto rootPath = Utility::noLeadingSlashPath(_propagator->remotePath());
const auto remoteFilename = _item->_encryptedFileName.isEmpty() ? _item->_file : _item->_encryptedFileName;
const auto remotePath = QString(rootPath + remoteFilename);
- const auto remoteParentPath = remotePath.left(remotePath.lastIndexOf('/'));
_remoteParentPath = remotePath.left(remotePath.lastIndexOf('/'));
const auto filenameInDb = _item->_file;
return _errorString;
}
-}
\ No newline at end of file
+}
qCDebug(lcPropagateRemoteMkdir) << "Success making the new folder encrypted";
propagator()->_activeJobList.removeOne(this);
_item->_e2eEncryptionStatus = encryptionStatus;
+ _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint();
_item->_e2eEncryptionStatusRemote = encryptionStatus;
if (_item->isEncrypted()) {
_item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(propagator()->account()->capabilities().clientSideEncryptionVersion());
if (quotaIt != propagator()->_folderQuota.end())
quotaIt.value() -= _fileToUpload._size;
+ if (_item->isEncrypted() && _uploadingEncrypted) {
+ _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint();
+ }
+
// Update the database entry
const auto result = propagator()->updateMetadata(*_item, Vfs::DatabaseMetadata);
if (!result) {
FileSystem::getSize(_completeFileName));
}
-} // namespace OCC
\ No newline at end of file
+} // namespace OCC
}
}
+ if (rec.isE2eEncrypted()) {
+ rec._e2eCertificateFingerprint = _account->encryptionCertificateFingerprint();
+ }
+
// Updating the db happens on success
if (!_journal->setFileRecord(rec)) {
item->_status = SyncFileItem::Status::NormalError;
for (const auto &e2EeLockedFolder : e2EeLockedFolders) {
const auto folderId = e2EeLockedFolder.first;
qCInfo(lcEngine()) << "start unlock job for folderId:" << folderId;
- const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->_privateKey, e2EeLockedFolder.second);
+ const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), e2EeLockedFolder.second);
+ if (!folderToken) {
+ qCWarning(lcEngine()) << "decrypt failed";
+ return;
+ }
// TODO: We need to rollback changes done to metadata in case we have an active lock, this needs to be implemented on the server first
- const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, folderToken, _journal, this);
+ const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, *folderToken, _journal, this);
unlockJob->setShouldRollbackMetadataChanges(true);
unlockJob->start();
}
rec._checksumHeader = _checksumHeader;
rec._e2eMangledName = _encryptedFileName.toUtf8();
rec._e2eEncryptionStatus = EncryptionStatusEnums::toDbEncryptionStatus(_e2eEncryptionStatus);
+ rec._e2eCertificateFingerprint = _e2eCertificateFingerprint;
rec._lockstate._locked = _locked == LockStatus::LockedItem;
rec._lockstate._lockOwnerDisplayName = _lockOwnerDisplayName;
rec._lockstate._lockOwnerId = _lockOwnerId;
item->_encryptedFileName = rec.e2eMangledName();
item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(rec._e2eEncryptionStatus);
item->_e2eEncryptionServerCapability = item->_e2eEncryptionStatus;
+ item->_e2eCertificateFingerprint = rec._e2eCertificateFingerprint;
item->_locked = rec._lockstate._locked ? LockStatus::LockedItem : LockStatus::UnlockedItem;
item->_lockOwnerDisplayName = rec._lockstate._lockOwnerDisplayName;
item->_lockOwnerId = rec._lockstate._lockOwnerId;
EncryptionStatus _e2eEncryptionStatus = EncryptionStatus::NotEncrypted; // The file is E2EE or the content of the directory should be E2EE
EncryptionStatus _e2eEncryptionServerCapability = EncryptionStatus::NotEncrypted;
EncryptionStatus _e2eEncryptionStatusRemote = EncryptionStatus::NotEncrypted;
+ QByteArray _e2eCertificateFingerprint;
quint16 _httpErrorCode = 0;
RemotePermissions _remotePerm;
QString _errorString; // Contains a string only in case of error
qCDebug(lcUpdateFileDropMetadataJob()) << "Error Getting the encrypted metadata.";
_item->_status = SyncFileItem::FatalError;
_item->_errorString = message;
- finished(SyncFileItem::FatalError);
+ emit finished(SyncFileItem::FatalError);
return;
}
propagator()->_journal->schedulePathForRemoteDiscovery(_item->_file);
propagator()->_anotherSyncNeeded = true;
_item->_status = itemStatus;
- finished(itemStatus);
+ emit finished(itemStatus);
}
void UpdateE2eeFolderMetadataJob::unlockFolder(const EncryptedFolderMetadataHandler::UnlockFolderWithResult result)
_item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(propagator()->account()->capabilities().clientSideEncryptionVersion());
}
}
- finished(itemStatus);
+ emit finished(itemStatus);
return;
}
qCDebug(lcUpdateFileDropMetadataJob) << "Calling Unlock";
- connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, [this](const QByteArray &folderId, int httpStatus) {
+ connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, _encryptedFolderMetadataHandler.data(), [this](const QByteArray &folderId, int httpStatus) {
if (httpStatus != 200) {
qCWarning(lcUpdateFileDropMetadataJob) << "Unlock Error" << folderId << httpStatus;
propagator()->account()->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
_item->_errorString = tr("Failed to unlock encrypted folder.");
- finished(SyncFileItem::FatalError);
+ emit finished(SyncFileItem::FatalError);
return;
}
|| !_encryptedFolderMetadataHandler->folderMetadata()->isValid()) {
qCWarning(lcUpdateFileDropMetadataJob) << "Failed to finalize item. Invalid metadata.";
_item->_errorString = tr("Failed to finalize item.");
- finished(SyncFileItem::FatalError);
+ emit finished(SyncFileItem::FatalError);
return;
}
_item->_e2eEncryptionStatus = _encryptedFolderMetadataHandler->folderMetadata()->encryptedMetadataEncryptionStatus();
_item->_e2eEncryptionStatusRemote = _encryptedFolderMetadataHandler->folderMetadata()->encryptedMetadataEncryptionStatus();
- finished(SyncFileItem::Success);
+ emit finished(SyncFileItem::Success);
});
_encryptedFolderMetadataHandler->unlockFolder(result);
}
const SyncFileItemPtr &syncFileItem,
const QString &fullRemotePath,
const QString &folderRemotePath)
- : PropagatorJob(propagator)
- , _item(syncFileItem)
+ : PropagateItemJob(propagator, syncFileItem)
, _fullRemotePath(fullRemotePath)
, _folderRemotePath(Utility::noLeadingSlashPath(Utility::noTrailingSlashPath(folderRemotePath)))
{
UpdateE2eeFolderUsersMetadataJob::Add,
_fullRemotePath,
propagator()->account()->davUser(),
- propagator()->account()->e2e()->_certificate);
+ propagator()->account()->e2e()->getCertificate());
updateMedatadaAndSubfoldersJob->setParent(this);
updateMedatadaAndSubfoldersJob->setSubJobSyncItems(_subJobItems);
_subJobItems.clear();
if (code == 200) {
_item->_e2eEncryptionStatus = updateMedatadaAndSubfoldersJob->encryptionStatus();
_item->_e2eEncryptionStatusRemote = updateMedatadaAndSubfoldersJob->encryptionStatus();
+ _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint();
+ propagator()->updateMetadata(*_item, Vfs::UpdateMetadataType::DatabaseMetadata);
emit finished(SyncFileItem::Status::Success);
} else {
_item->_errorString = message;
class FolderMetadata;
-class OWNCLOUDSYNC_EXPORT UpdateMigratedE2eeMetadataJob : public PropagatorJob
+class OWNCLOUDSYNC_EXPORT UpdateMigratedE2eeMetadataJob : public PropagateItemJob
{
Q_OBJECT
public:
- explicit UpdateMigratedE2eeMetadataJob(OwncloudPropagator *propagator, const SyncFileItemPtr &syncFileItem, const QString &path, const QString &folderRemotePath);
+ explicit UpdateMigratedE2eeMetadataJob(OwncloudPropagator *propagator, const SyncFileItemPtr &syncFileItem, const QString &fullRemotePath, const QString &folderRemotePath);
[[nodiscard]] bool scheduleSelfOrChild() override;
void addSubJobItem(const QString &key, const SyncFileItemPtr &syncFileItem);
private slots:
- void start();
+ void start() override;
private:
- SyncFileItemPtr _item;
QHash<QString, SyncFileItemPtr> _subJobItems;
QString _fullRemotePath;
QString _folderRemotePath;
QVERIFY(!publicKey.isNull());
QVERIFY(!privateKey.isEmpty());
- _account->e2e()->_certificate = cert;
- _account->e2e()->_publicKey = publicKey;
- _account->e2e()->_privateKey = privateKey;
+ _account->e2e()->setCertificate(cert);
+ _account->e2e()->setPrivateKey(privateKey);
- _secondAccount->e2e()->_certificate = cert;
- _secondAccount->e2e()->_publicKey = publicKey;
- _secondAccount->e2e()->_privateKey = privateKey;
+ _secondAccount->e2e()->setCertificate(cert);
+ _secondAccount->e2e()->setPrivateKey(privateKey);
}
}
const auto certificatePem = folderUserObject.value("certificate").toString().toUtf8();
+ const auto certificate = QSslCertificate{certificatePem};
const auto encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value("encryptedMetadataKey").toString().toUtf8());
if (!encryptedMetadataKey.isEmpty()) {
- const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey);
+ const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey, certificate.digest(QCryptographicHash::Sha256));
if (decryptedMetadataKey.isEmpty()) {
break;
}
encryptedFile.initializationVector = EncryptionHelper::generateRandom(16);
metadata->addEncryptedFile(encryptedFile);
- QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->_certificate));
+ QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->getCertificate()));
QVERIFY(metadata->removeUser(_secondAccount->davUser()));
- QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->_certificate));
+ QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->getCertificate()));
const auto encryptedMetadata = metadata->encryptedMetadata();
QVERIFY(!encryptedMetadata.isEmpty());
}
const auto certificatePem = folderUserObject.value("certificate").toString().toUtf8();
+ const auto certificate = QSslCertificate{certificatePem};
const auto encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value("encryptedMetadataKey").toString().toUtf8());
if (!encryptedMetadataKey.isEmpty()) {
- const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey);
+ const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey, certificate.digest(QCryptographicHash::Sha256));
if (decryptedMetadataKey.isEmpty()) {
break;
}
}
const auto certificatePem = folderUserObject.value("certificate").toString().toUtf8();
+ const auto certificate = QSslCertificate{certificatePem};
const auto encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value("encryptedMetadataKey").toString().toUtf8());
if (!encryptedMetadataKey.isEmpty()) {
- const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey);
+ const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey, certificate.digest(QCryptographicHash::Sha256));
if (decryptedMetadataKey.isEmpty()) {
break;
}
QVERIFY(!publicKey.isNull());
QVERIFY(!privateKey.isEmpty());
- _account->e2e()->_certificate = cert;
- _account->e2e()->_publicKey = publicKey;
- _account->e2e()->_privateKey = privateKey;
+ _account->e2e()->setCertificate(cert);
+ _account->e2e()->setPrivateKey(privateKey);
QScopedPointer<FolderMetadata> metadata(new FolderMetadata(_account, "/", FolderMetadata::FolderType::Root));
QSignalSpy metadataSetupCompleteSpy(metadata.data(), &FolderMetadata::setupComplete);